| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  | import { useCallback, useEffect, useRef, useState } from 'react' | 
					
						
							|  |  |  | import { useTranslation } from 'react-i18next' | 
					
						
							|  |  |  | import { useParams, usePathname } from 'next/navigation' | 
					
						
							| 
									
										
										
										
											2024-06-20 11:05:08 +08:00
										 |  |  | import { | 
					
						
							|  |  |  |   RiCloseLine, | 
					
						
							|  |  |  |   RiLoader2Line, | 
					
						
							|  |  |  | } from '@remixicon/react' | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  | import Recorder from 'js-audio-recorder' | 
					
						
							|  |  |  | import { useRafInterval } from 'ahooks' | 
					
						
							| 
									
										
										
										
											2023-07-12 17:18:56 +08:00
										 |  |  | import { convertToMp3 } from './utils' | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  | import s from './index.module.css' | 
					
						
							| 
									
										
										
										
											2024-07-09 15:05:40 +08:00
										 |  |  | import cn from '@/utils/classnames' | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  | import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' | 
					
						
							|  |  |  | import { audioToText } from '@/service/share' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type VoiceInputTypes = { | 
					
						
							|  |  |  |   onConverted: (text: string) => void | 
					
						
							|  |  |  |   onCancel: () => void | 
					
						
							| 
									
										
										
										
											2024-09-05 21:00:09 +08:00
										 |  |  |   wordTimestamps?: string | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const VoiceInput = ({ | 
					
						
							|  |  |  |   onCancel, | 
					
						
							|  |  |  |   onConverted, | 
					
						
							| 
									
										
										
										
											2024-09-05 21:00:09 +08:00
										 |  |  |   wordTimestamps, | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  | }: VoiceInputTypes) => { | 
					
						
							|  |  |  |   const { t } = useTranslation() | 
					
						
							| 
									
										
										
										
											2023-07-12 17:18:56 +08:00
										 |  |  |   const recorder = useRef(new Recorder({ | 
					
						
							|  |  |  |     sampleBits: 16, | 
					
						
							|  |  |  |     sampleRate: 16000, | 
					
						
							|  |  |  |     numChannels: 1, | 
					
						
							|  |  |  |     compiling: false, | 
					
						
							|  |  |  |   })) | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  |   const canvasRef = useRef<HTMLCanvasElement | null>(null) | 
					
						
							|  |  |  |   const ctxRef = useRef<CanvasRenderingContext2D | null>(null) | 
					
						
							|  |  |  |   const drawRecordId = useRef<number | null>(null) | 
					
						
							|  |  |  |   const [originDuration, setOriginDuration] = useState(0) | 
					
						
							|  |  |  |   const [startRecord, setStartRecord] = useState(false) | 
					
						
							|  |  |  |   const [startConvert, setStartConvert] = useState(false) | 
					
						
							|  |  |  |   const pathname = usePathname() | 
					
						
							|  |  |  |   const params = useParams() | 
					
						
							|  |  |  |   const clearInterval = useRafInterval(() => { | 
					
						
							|  |  |  |     setOriginDuration(originDuration + 1) | 
					
						
							|  |  |  |   }, 1000) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const drawRecord = useCallback(() => { | 
					
						
							|  |  |  |     drawRecordId.current = requestAnimationFrame(drawRecord) | 
					
						
							|  |  |  |     const canvas = canvasRef.current! | 
					
						
							|  |  |  |     const ctx = ctxRef.current! | 
					
						
							|  |  |  |     const dataUnit8Array = recorder.current.getRecordAnalyseData() | 
					
						
							|  |  |  |     const dataArray = [].slice.call(dataUnit8Array) | 
					
						
							|  |  |  |     const lineLength = parseInt(`${canvas.width / 3}`) | 
					
						
							|  |  |  |     const gap = parseInt(`${1024 / lineLength}`) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     ctx.clearRect(0, 0, canvas.width, canvas.height) | 
					
						
							|  |  |  |     ctx.beginPath() | 
					
						
							|  |  |  |     let x = 0 | 
					
						
							|  |  |  |     for (let i = 0; i < lineLength; i++) { | 
					
						
							|  |  |  |       let v = dataArray.slice(i * gap, i * gap + gap).reduce((prev: number, next: number) => { | 
					
						
							|  |  |  |         return prev + next | 
					
						
							|  |  |  |       }, 0) / gap | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (v < 128) | 
					
						
							|  |  |  |         v = 128 | 
					
						
							|  |  |  |       if (v > 178) | 
					
						
							|  |  |  |         v = 178 | 
					
						
							|  |  |  |       const y = (v - 128) / 50 * canvas.height | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       ctx.moveTo(x, 16) | 
					
						
							| 
									
										
										
										
											2023-07-10 10:16:38 +08:00
										 |  |  |       if (ctx.roundRect) | 
					
						
							|  |  |  |         ctx.roundRect(x, 16 - y, 2, y, [1, 1, 0, 0]) | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         ctx.rect(x, 16 - y, 2, y) | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  |       ctx.fill() | 
					
						
							|  |  |  |       x += 3 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     ctx.closePath() | 
					
						
							|  |  |  |   }, []) | 
					
						
							|  |  |  |   const handleStopRecorder = useCallback(async () => { | 
					
						
							|  |  |  |     clearInterval() | 
					
						
							|  |  |  |     setStartRecord(false) | 
					
						
							|  |  |  |     setStartConvert(true) | 
					
						
							|  |  |  |     recorder.current.stop() | 
					
						
							|  |  |  |     drawRecordId.current && cancelAnimationFrame(drawRecordId.current) | 
					
						
							|  |  |  |     drawRecordId.current = null | 
					
						
							|  |  |  |     const canvas = canvasRef.current! | 
					
						
							|  |  |  |     const ctx = ctxRef.current! | 
					
						
							|  |  |  |     ctx.clearRect(0, 0, canvas.width, canvas.height) | 
					
						
							| 
									
										
										
										
											2023-07-12 17:18:56 +08:00
										 |  |  |     const mp3Blob = convertToMp3(recorder.current) | 
					
						
							|  |  |  |     const mp3File = new File([mp3Blob], 'temp.mp3', { type: 'audio/mp3' }) | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  |     const formData = new FormData() | 
					
						
							| 
									
										
										
										
											2023-07-12 17:18:56 +08:00
										 |  |  |     formData.append('file', mp3File) | 
					
						
							| 
									
										
										
										
											2024-09-05 21:00:09 +08:00
										 |  |  |     formData.append('word_timestamps', wordTimestamps || 'disabled') | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-25 11:47:35 +08:00
										 |  |  |     let url = '' | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  |     let isPublic = false | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (params.token) { | 
					
						
							|  |  |  |       url = '/audio-to-text' | 
					
						
							|  |  |  |       isPublic = true | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     else if (params.appId) { | 
					
						
							|  |  |  |       if (pathname.search('explore/installed') > -1) | 
					
						
							|  |  |  |         url = `/installed-apps/${params.appId}/audio-to-text` | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         url = `/apps/${params.appId}/audio-to-text` | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       const audioResponse = await audioToText(url, isPublic, formData) | 
					
						
							|  |  |  |       onConverted(audioResponse.text) | 
					
						
							|  |  |  |       onCancel() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     catch (e) { | 
					
						
							|  |  |  |       onConverted('') | 
					
						
							|  |  |  |       onCancel() | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-09-05 21:00:09 +08:00
										 |  |  |   }, [clearInterval, onCancel, onConverted, params.appId, params.token, pathname, wordTimestamps]) | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  |   const handleStartRecord = async () => { | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       await recorder.current.start() | 
					
						
							|  |  |  |       setStartRecord(true) | 
					
						
							|  |  |  |       setStartConvert(false) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (canvasRef.current && ctxRef.current) | 
					
						
							|  |  |  |         drawRecord() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     catch (e) { | 
					
						
							|  |  |  |       onCancel() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const initCanvas = () => { | 
					
						
							|  |  |  |     const dpr = window.devicePixelRatio || 1 | 
					
						
							|  |  |  |     const canvas = document.getElementById('voice-input-record') as HTMLCanvasElement | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (canvas) { | 
					
						
							|  |  |  |       const { width: cssWidth, height: cssHeight } = canvas.getBoundingClientRect() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       canvas.width = dpr * cssWidth | 
					
						
							|  |  |  |       canvas.height = dpr * cssHeight | 
					
						
							|  |  |  |       canvasRef.current = canvas | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const ctx = canvas.getContext('2d') | 
					
						
							|  |  |  |       if (ctx) { | 
					
						
							|  |  |  |         ctx.scale(dpr, dpr) | 
					
						
							|  |  |  |         ctx.fillStyle = 'rgba(209, 224, 255, 1)' | 
					
						
							|  |  |  |         ctxRef.current = ctx | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2024-09-05 21:00:09 +08:00
										 |  |  |   if (originDuration >= 600 && startRecord) | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  |     handleStopRecorder() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     initCanvas() | 
					
						
							|  |  |  |     handleStartRecord() | 
					
						
							| 
									
										
										
										
											2024-07-23 17:24:29 +08:00
										 |  |  |     const recorderRef = recorder?.current | 
					
						
							|  |  |  |     return () => { | 
					
						
							|  |  |  |       recorderRef?.stop() | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  |   }, []) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const minutes = parseInt(`${parseInt(`${originDuration}`) / 60}`) | 
					
						
							|  |  |  |   const seconds = parseInt(`${originDuration}`) % 60 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return ( | 
					
						
							|  |  |  |     <div className={cn(s.wrapper, 'absolute inset-0 rounded-xl')}> | 
					
						
							|  |  |  |       <div className='absolute inset-[1.5px] flex items-center pl-[14.5px] pr-[6.5px] py-[14px] bg-primary-25 rounded-[10.5px] overflow-hidden'> | 
					
						
							|  |  |  |         <canvas id='voice-input-record' className='absolute left-0 bottom-0 w-full h-4' /> | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2024-06-20 11:05:08 +08:00
										 |  |  |           startConvert && <RiLoader2Line className='animate-spin mr-2 w-4 h-4 text-primary-700' /> | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  |         } | 
					
						
							|  |  |  |         <div className='grow'> | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             startRecord && ( | 
					
						
							|  |  |  |               <div className='text-sm text-gray-500'> | 
					
						
							|  |  |  |                 {t('common.voiceInput.speaking')} | 
					
						
							|  |  |  |               </div> | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             startConvert && ( | 
					
						
							|  |  |  |               <div className={cn(s.convert, 'text-sm')}> | 
					
						
							|  |  |  |                 {t('common.voiceInput.converting')} | 
					
						
							|  |  |  |               </div> | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         </div> | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           startRecord && ( | 
					
						
							|  |  |  |             <div | 
					
						
							|  |  |  |               className='flex justify-center items-center mr-1 w-8 h-8 hover:bg-primary-100 rounded-lg  cursor-pointer' | 
					
						
							|  |  |  |               onClick={handleStopRecorder} | 
					
						
							|  |  |  |             > | 
					
						
							|  |  |  |               <StopCircle className='w-5 h-5 text-primary-600' /> | 
					
						
							|  |  |  |             </div> | 
					
						
							|  |  |  |           ) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           startConvert && ( | 
					
						
							|  |  |  |             <div | 
					
						
							|  |  |  |               className='flex justify-center items-center mr-1 w-8 h-8 hover:bg-gray-200 rounded-lg  cursor-pointer' | 
					
						
							|  |  |  |               onClick={onCancel} | 
					
						
							|  |  |  |             > | 
					
						
							| 
									
										
										
										
											2024-06-20 11:05:08 +08:00
										 |  |  |               <RiCloseLine className='w-4 h-4 text-gray-500' /> | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  |             </div> | 
					
						
							|  |  |  |           ) | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-09-05 21:00:09 +08:00
										 |  |  |         <div className={`w-[45px] pl-1 text-xs font-medium ${originDuration > 500 ? 'text-[#F04438]' : 'text-gray-700'}`}>{`0${minutes.toFixed(0)}:${seconds >= 10 ? seconds : `0${seconds}`}`}</div> | 
					
						
							| 
									
										
										
										
											2023-07-07 17:50:42 +08:00
										 |  |  |       </div> | 
					
						
							|  |  |  |     </div> | 
					
						
							|  |  |  |   ) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export default VoiceInput |