index.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { useCallback, useEffect, useRef, useState } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import { useParams, usePathname } from 'next/navigation'
  4. import cn from 'classnames'
  5. import Recorder from 'js-audio-recorder'
  6. import { useRafInterval } from 'ahooks'
  7. import s from './index.module.css'
  8. import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
  9. import { Loading02, XClose } from '@/app/components/base/icons/src/vender/line/general'
  10. import { audioToText } from '@/service/share'
  11. type VoiceInputTypes = {
  12. onConverted: (text: string) => void
  13. onCancel: () => void
  14. }
  15. const VoiceInput = ({
  16. onCancel,
  17. onConverted,
  18. }: VoiceInputTypes) => {
  19. const { t } = useTranslation()
  20. const recorder = useRef(new Recorder())
  21. const canvasRef = useRef<HTMLCanvasElement | null>(null)
  22. const ctxRef = useRef<CanvasRenderingContext2D | null>(null)
  23. const drawRecordId = useRef<number | null>(null)
  24. const [originDuration, setOriginDuration] = useState(0)
  25. const [startRecord, setStartRecord] = useState(false)
  26. const [startConvert, setStartConvert] = useState(false)
  27. const pathname = usePathname()
  28. const params = useParams()
  29. const clearInterval = useRafInterval(() => {
  30. setOriginDuration(originDuration + 1)
  31. }, 1000)
  32. const drawRecord = useCallback(() => {
  33. drawRecordId.current = requestAnimationFrame(drawRecord)
  34. const canvas = canvasRef.current!
  35. const ctx = ctxRef.current!
  36. const dataUnit8Array = recorder.current.getRecordAnalyseData()
  37. const dataArray = [].slice.call(dataUnit8Array)
  38. const lineLength = parseInt(`${canvas.width / 3}`)
  39. const gap = parseInt(`${1024 / lineLength}`)
  40. ctx.clearRect(0, 0, canvas.width, canvas.height)
  41. ctx.beginPath()
  42. let x = 0
  43. for (let i = 0; i < lineLength; i++) {
  44. let v = dataArray.slice(i * gap, i * gap + gap).reduce((prev: number, next: number) => {
  45. return prev + next
  46. }, 0) / gap
  47. if (v < 128)
  48. v = 128
  49. if (v > 178)
  50. v = 178
  51. const y = (v - 128) / 50 * canvas.height
  52. ctx.moveTo(x, 16)
  53. if (ctx.roundRect)
  54. ctx.roundRect(x, 16 - y, 2, y, [1, 1, 0, 0])
  55. else
  56. ctx.rect(x, 16 - y, 2, y)
  57. ctx.fill()
  58. x += 3
  59. }
  60. ctx.closePath()
  61. }, [])
  62. const handleStopRecorder = useCallback(async () => {
  63. clearInterval()
  64. setStartRecord(false)
  65. setStartConvert(true)
  66. recorder.current.stop()
  67. drawRecordId.current && cancelAnimationFrame(drawRecordId.current)
  68. drawRecordId.current = null
  69. const canvas = canvasRef.current!
  70. const ctx = ctxRef.current!
  71. ctx.clearRect(0, 0, canvas.width, canvas.height)
  72. const wavBlob = recorder.current.getWAVBlob()
  73. const wavFile = new File([wavBlob], 'a.wav', { type: 'audio/wav' })
  74. const formData = new FormData()
  75. formData.append('file', wavFile)
  76. let url = ''
  77. let isPublic = false
  78. if (params.token) {
  79. url = '/audio-to-text'
  80. isPublic = true
  81. }
  82. else if (params.appId) {
  83. if (pathname.search('explore/installed') > -1)
  84. url = `/installed-apps/${params.appId}/audio-to-text`
  85. else
  86. url = `/apps/${params.appId}/audio-to-text`
  87. }
  88. try {
  89. const audioResponse = await audioToText(url, isPublic, formData)
  90. onConverted(audioResponse.text)
  91. onCancel()
  92. }
  93. catch (e) {
  94. onConverted('')
  95. onCancel()
  96. }
  97. }, [])
  98. const handleStartRecord = async () => {
  99. try {
  100. await recorder.current.start()
  101. setStartRecord(true)
  102. setStartConvert(false)
  103. if (canvasRef.current && ctxRef.current)
  104. drawRecord()
  105. }
  106. catch (e) {
  107. onCancel()
  108. }
  109. }
  110. const initCanvas = () => {
  111. const dpr = window.devicePixelRatio || 1
  112. const canvas = document.getElementById('voice-input-record') as HTMLCanvasElement
  113. if (canvas) {
  114. const { width: cssWidth, height: cssHeight } = canvas.getBoundingClientRect()
  115. canvas.width = dpr * cssWidth
  116. canvas.height = dpr * cssHeight
  117. canvasRef.current = canvas
  118. const ctx = canvas.getContext('2d')
  119. if (ctx) {
  120. ctx.scale(dpr, dpr)
  121. ctx.fillStyle = 'rgba(209, 224, 255, 1)'
  122. ctxRef.current = ctx
  123. }
  124. }
  125. }
  126. if (originDuration >= 120 && startRecord)
  127. handleStopRecorder()
  128. useEffect(() => {
  129. initCanvas()
  130. handleStartRecord()
  131. }, [])
  132. const minutes = parseInt(`${parseInt(`${originDuration}`) / 60}`)
  133. const seconds = parseInt(`${originDuration}`) % 60
  134. return (
  135. <div className={cn(s.wrapper, 'absolute inset-0 rounded-xl')}>
  136. <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'>
  137. <canvas id='voice-input-record' className='absolute left-0 bottom-0 w-full h-4' />
  138. {
  139. startConvert && <Loading02 className='animate-spin mr-2 w-4 h-4 text-primary-700' />
  140. }
  141. <div className='grow'>
  142. {
  143. startRecord && (
  144. <div className='text-sm text-gray-500'>
  145. {t('common.voiceInput.speaking')}
  146. </div>
  147. )
  148. }
  149. {
  150. startConvert && (
  151. <div className={cn(s.convert, 'text-sm')}>
  152. {t('common.voiceInput.converting')}
  153. </div>
  154. )
  155. }
  156. </div>
  157. {
  158. startRecord && (
  159. <div
  160. className='flex justify-center items-center mr-1 w-8 h-8 hover:bg-primary-100 rounded-lg cursor-pointer'
  161. onClick={handleStopRecorder}
  162. >
  163. <StopCircle className='w-5 h-5 text-primary-600' />
  164. </div>
  165. )
  166. }
  167. {
  168. startConvert && (
  169. <div
  170. className='flex justify-center items-center mr-1 w-8 h-8 hover:bg-gray-200 rounded-lg cursor-pointer'
  171. onClick={onCancel}
  172. >
  173. <XClose className='w-4 h-4 text-gray-500' />
  174. </div>
  175. )
  176. }
  177. <div className={`w-[45px] pl-1 text-xs font-medium ${originDuration > 110 ? 'text-[#F04438]' : 'text-gray-700'}`}>{`0${minutes.toFixed(0)}:${seconds >= 10 ? seconds : `0${seconds}`}`}</div>
  178. </div>
  179. </div>
  180. )
  181. }
  182. export default VoiceInput