use new audio recorder
This commit is contained in:
parent
20c345f05a
commit
de70929a62
@ -34,7 +34,8 @@
|
|||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.0.12",
|
"tailwindcss": "^4.0.12",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vite-plugin-pwa": "^0.21.1"
|
"vite-plugin-pwa": "^0.21.1",
|
||||||
|
"vmsg": "^0.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.21.0",
|
||||||
|
|||||||
8
frontend/pnpm-lock.yaml
generated
8
frontend/pnpm-lock.yaml
generated
@ -80,6 +80,9 @@ importers:
|
|||||||
vite-plugin-pwa:
|
vite-plugin-pwa:
|
||||||
specifier: ^0.21.1
|
specifier: ^0.21.1
|
||||||
version: 0.21.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
|
version: 0.21.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
|
||||||
|
vmsg:
|
||||||
|
specifier: ^0.4.0
|
||||||
|
version: 0.4.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.21.0
|
specifier: ^9.21.0
|
||||||
@ -2976,6 +2979,9 @@ packages:
|
|||||||
yaml:
|
yaml:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vmsg@0.4.0:
|
||||||
|
resolution: {integrity: sha512-46BBqRSfqdFGUpO2j+Hpz8T9YE5uWG0/PWal1PT+R1o8NEthtjG/XWl4HzbB8hIHpg/UtmKvsxL2OKQBrIYcHQ==}
|
||||||
|
|
||||||
webidl-conversions@4.0.2:
|
webidl-conversions@4.0.2:
|
||||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||||
|
|
||||||
@ -6036,6 +6042,8 @@ snapshots:
|
|||||||
lightningcss: 1.29.2
|
lightningcss: 1.29.2
|
||||||
terser: 5.39.0
|
terser: 5.39.0
|
||||||
|
|
||||||
|
vmsg@0.4.0: {}
|
||||||
|
|
||||||
webidl-conversions@4.0.2: {}
|
webidl-conversions@4.0.2: {}
|
||||||
|
|
||||||
whatwg-url@7.1.0:
|
whatwg-url@7.1.0:
|
||||||
|
|||||||
81
frontend/src/hooks/useAudioRecorder.ts
Normal file
81
frontend/src/hooks/useAudioRecorder.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// useAudioRecorder.ts
|
||||||
|
import { useState, useCallback, useEffect } from 'react'
|
||||||
|
import vmsg from 'vmsg'
|
||||||
|
|
||||||
|
// Initialize the recorder once
|
||||||
|
const recorder = new vmsg.Recorder({
|
||||||
|
wasmURL: "https://unpkg.com/vmsg@0.3.0/vmsg.wasm"
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useAudioRecorder() {
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [isRecording, setIsRecording] = useState(false)
|
||||||
|
const [recordings, setRecordings] = useState<string[]>([])
|
||||||
|
const [currentRecording, setCurrentRecording] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const startRecording = useCallback(async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
await recorder.initAudio()
|
||||||
|
await recorder.initWorker()
|
||||||
|
recorder.startRecording()
|
||||||
|
setIsRecording(true)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to start recording:', e)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const stopRecording = useCallback(async () => {
|
||||||
|
if (!isRecording) return
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const blob = await recorder.stopRecording()
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
setRecordings(prev => [...prev, url])
|
||||||
|
setCurrentRecording(url)
|
||||||
|
return { blob, url }
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to stop recording:', e)
|
||||||
|
} finally {
|
||||||
|
setIsRecording(false)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [isRecording])
|
||||||
|
|
||||||
|
const toggleRecording = useCallback(async () => {
|
||||||
|
if (isRecording) {
|
||||||
|
return await stopRecording()
|
||||||
|
} else {
|
||||||
|
await startRecording()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}, [isRecording, startRecording, stopRecording])
|
||||||
|
|
||||||
|
const clearRecordings = useCallback(() => {
|
||||||
|
// Revoke object URLs to prevent memory leaks
|
||||||
|
recordings.forEach(url => URL.revokeObjectURL(url))
|
||||||
|
setRecordings([])
|
||||||
|
setCurrentRecording(null)
|
||||||
|
}, [recordings])
|
||||||
|
|
||||||
|
// Clean up object URLs when component unmounts
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
recordings.forEach(url => URL.revokeObjectURL(url))
|
||||||
|
}
|
||||||
|
}, [recordings])
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
isRecording,
|
||||||
|
recordings,
|
||||||
|
currentRecording,
|
||||||
|
startRecording,
|
||||||
|
stopRecording,
|
||||||
|
toggleRecording,
|
||||||
|
clearRecordings
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
import { recipeService } from "@/api/recipe"
|
import { recipeService } from "@/api/recipe"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@ -11,40 +11,42 @@ import { Mic, Upload, ArrowLeft, Loader2 } from "lucide-react"
|
|||||||
import { motion } from "framer-motion"
|
import { motion } from "framer-motion"
|
||||||
import { KitchenIllustration } from "@/components/illustrations/KitchenIllustration"
|
import { KitchenIllustration } from "@/components/illustrations/KitchenIllustration"
|
||||||
import { CookingLoader } from "@/components/illustrations/CookingLoader"
|
import { CookingLoader } from "@/components/illustrations/CookingLoader"
|
||||||
|
import { useAudioRecorder } from "@/hooks/useAudioRecorder"
|
||||||
|
|
||||||
export default function RecipeForm() {
|
export default function RecipeForm() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading: isRecorderLoading,
|
||||||
|
isRecording,
|
||||||
|
currentRecording,
|
||||||
|
startRecording,
|
||||||
|
stopRecording
|
||||||
|
} = useAudioRecorder()
|
||||||
|
|
||||||
const [audioFile, setAudioFile] = useState<File | null>(null)
|
const [audioFile, setAudioFile] = useState<File | null>(null)
|
||||||
const [isRecording, setIsRecording] = useState(false)
|
|
||||||
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null)
|
|
||||||
const [recordingStatus, setRecordingStatus] = useState<"idle" | "recording" | "processing">("idle")
|
const [recordingStatus, setRecordingStatus] = useState<"idle" | "recording" | "processing">("idle")
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
|
|
||||||
|
// Update audioFile when recording is available
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentRecording) {
|
||||||
|
fetch(currentRecording)
|
||||||
|
.then(res => res.blob())
|
||||||
|
.then(blob => {
|
||||||
|
const file = new File([blob], "recording.mp3", { type: "audio/mp3" })
|
||||||
|
setAudioFile(file)
|
||||||
|
setRecordingStatus("idle")
|
||||||
|
setError("")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [currentRecording])
|
||||||
|
|
||||||
// Démarrer l'enregistrement audio
|
// Handle recording start
|
||||||
const startRecording = async () => {
|
const handleStartRecording = async () => {
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
await startRecording()
|
||||||
const recorder = new MediaRecorder(stream)
|
|
||||||
setMediaRecorder(recorder)
|
|
||||||
|
|
||||||
const chunks: BlobPart[] = []
|
|
||||||
recorder.ondataavailable = (e) => {
|
|
||||||
chunks.push(e.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
recorder.onstop = () => {
|
|
||||||
const blob = new Blob(chunks, { type: "audio/webm" })
|
|
||||||
const file = new File([blob], "recording.webm", { type: "audio/webm" })
|
|
||||||
setAudioFile(file)
|
|
||||||
setRecordingStatus("idle")
|
|
||||||
setError("")
|
|
||||||
}
|
|
||||||
|
|
||||||
recorder.start()
|
|
||||||
setIsRecording(true)
|
|
||||||
setRecordingStatus("recording")
|
setRecordingStatus("recording")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erreur lors de l'accès au microphone:", err)
|
console.error("Erreur lors de l'accès au microphone:", err)
|
||||||
@ -52,15 +54,12 @@ export default function RecipeForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arrêter l'enregistrement audio
|
// Handle recording stop
|
||||||
const stopRecording = () => {
|
const handleStopRecording = async () => {
|
||||||
if (mediaRecorder && isRecording) {
|
if (isRecording) {
|
||||||
mediaRecorder.stop()
|
await stopRecording()
|
||||||
setIsRecording(false)
|
|
||||||
setRecordingStatus("processing")
|
setRecordingStatus("processing")
|
||||||
|
// Processing will be updated to "idle" when the audioFile is set
|
||||||
// Arrêter toutes les pistes audio
|
|
||||||
mediaRecorder.stream.getTracks().forEach((track) => track.stop())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +178,7 @@ export default function RecipeForm() {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={startRecording}
|
onClick={handleStartRecording}
|
||||||
>
|
>
|
||||||
<Mic className="mr-2 h-4 w-4" />
|
<Mic className="mr-2 h-4 w-4" />
|
||||||
Commencer l'enregistrement
|
Commencer l'enregistrement
|
||||||
@ -190,7 +189,7 @@ export default function RecipeForm() {
|
|||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={stopRecording}
|
onClick={handleStopRecording}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user