add more data + image on recipe
This commit is contained in:
parent
bd5891ff1b
commit
223b593495
@ -0,0 +1,8 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Recipe" ADD COLUMN "cookingTime" INTEGER;
|
||||||
|
ALTER TABLE "Recipe" ADD COLUMN "difficulty" TEXT;
|
||||||
|
ALTER TABLE "Recipe" ADD COLUMN "imageUrl" TEXT;
|
||||||
|
ALTER TABLE "Recipe" ADD COLUMN "preparationTime" INTEGER;
|
||||||
|
ALTER TABLE "Recipe" ADD COLUMN "servings" INTEGER;
|
||||||
|
ALTER TABLE "Recipe" ADD COLUMN "steps" TEXT;
|
||||||
|
ALTER TABLE "Recipe" ADD COLUMN "tips" TEXT;
|
||||||
@ -21,14 +21,21 @@ model User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Recipe {
|
model Recipe {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
title String
|
title String
|
||||||
ingredients String
|
ingredients String
|
||||||
userPrompt String
|
userPrompt String
|
||||||
generatedRecipe String
|
generatedRecipe String
|
||||||
audioUrl String?
|
audioUrl String?
|
||||||
userId String
|
imageUrl String?
|
||||||
user User @relation(fields: [userId], references: [id])
|
preparationTime Int?
|
||||||
createdAt DateTime @default(now())
|
cookingTime Int?
|
||||||
updatedAt DateTime @updatedAt
|
servings Int?
|
||||||
|
difficulty String?
|
||||||
|
steps String?
|
||||||
|
tips String?
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
@ -24,20 +24,34 @@ module.exports = fp(async function (fastify, opts) {
|
|||||||
// Génération de recette avec 01-mini
|
// Génération de recette avec 01-mini
|
||||||
fastify.decorate('generateRecipe', async (ingredients, prompt) => {
|
fastify.decorate('generateRecipe', async (ingredients, prompt) => {
|
||||||
const completion = await openai.chat.completions.create({
|
const completion = await openai.chat.completions.create({
|
||||||
model: "gpt-3.5-turbo", // Remplacer par 01-mini quand disponible
|
model: "gpt-4o-mini", // Remplacer par 01-mini quand disponible
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
content: "Tu es un chef cuisinier expert qui crée des recettes délicieuses et faciles à réaliser."
|
content: "Tu es un chef cuisinier expert qui crée des recettes délicieuses et faciles à réaliser. Tu dois toujours répondre avec un objet JSON valide contenant les champs suivants: titre, ingredients, etapes, temps_preparation (en minutes), temps_cuisson (en minutes), portions, difficulte (facile, moyen, difficile), et conseils."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: `Voici les ingrédients disponibles: ${ingredients}. ${prompt || 'Propose une recette avec ces ingrédients.'}`
|
content: `Voici les ingrédients disponibles: ${ingredients}. ${prompt || 'Propose une recette avec ces ingrédients.'} Réponds uniquement avec un objet JSON.`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
response_format: { type: "json_object" },
|
||||||
});
|
});
|
||||||
|
|
||||||
return completion.choices[0].message.content;
|
const recipeData = JSON.parse(completion.choices[0].message.content);
|
||||||
|
|
||||||
|
// Génération de l'image du plat
|
||||||
|
const imageResponse = await openai.images.generate({
|
||||||
|
model: "dall-e-3",
|
||||||
|
prompt: `Une photo culinaire professionnelle et appétissante du plat "${recipeData.titre}". Le plat est présenté sur une belle assiette, avec un éclairage professionnel, style photographie gastronomique.`,
|
||||||
|
n: 1,
|
||||||
|
size: "1024x1024",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ajouter l'URL de l'image à l'objet recette
|
||||||
|
recipeData.image_url = imageResponse.data[0].url;
|
||||||
|
|
||||||
|
return recipeData;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gestion du téléchargement de fichiers audio
|
// Gestion du téléchargement de fichiers audio
|
||||||
|
|||||||
@ -47,15 +47,31 @@ module.exports = async function (fastify, opts) {
|
|||||||
const ingredients = transcription;
|
const ingredients = transcription;
|
||||||
|
|
||||||
// Générer la recette
|
// Générer la recette
|
||||||
const generatedRecipe = await fastify.generateRecipe(ingredients, "Crée une recette délicieuse et détaillée");
|
const recipeData = await fastify.generateRecipe(ingredients, "Crée une recette délicieuse et détaillée");
|
||||||
|
|
||||||
|
// Convertir les tableaux en chaînes de caractères pour la base de données
|
||||||
|
const ingredientsString = Array.isArray(recipeData.ingredients)
|
||||||
|
? recipeData.ingredients.join('\n')
|
||||||
|
: recipeData.ingredients;
|
||||||
|
|
||||||
|
const stepsString = Array.isArray(recipeData.etapes)
|
||||||
|
? recipeData.etapes.join('\n')
|
||||||
|
: recipeData.etapes;
|
||||||
|
|
||||||
// Créer la recette en base de données
|
// Créer la recette en base de données
|
||||||
const recipe = await fastify.prisma.recipe.create({
|
const recipe = await fastify.prisma.recipe.create({
|
||||||
data: {
|
data: {
|
||||||
title: `Recette du ${new Date().toLocaleDateString()}`,
|
title: recipeData.titre,
|
||||||
ingredients,
|
ingredients: ingredientsString,
|
||||||
userPrompt: transcription,
|
userPrompt: transcription,
|
||||||
generatedRecipe,
|
generatedRecipe: JSON.stringify(recipeData),
|
||||||
|
imageUrl: recipeData.image_url,
|
||||||
|
preparationTime: recipeData.temps_preparation,
|
||||||
|
cookingTime: recipeData.temps_cuisson,
|
||||||
|
servings: recipeData.portions,
|
||||||
|
difficulty: recipeData.difficulte,
|
||||||
|
steps: stepsString,
|
||||||
|
tips: recipeData.conseils,
|
||||||
audioUrl: audioPath,
|
audioUrl: audioPath,
|
||||||
userId: request.user.id
|
userId: request.user.id
|
||||||
}
|
}
|
||||||
@ -66,7 +82,13 @@ module.exports = async function (fastify, opts) {
|
|||||||
id: recipe.id,
|
id: recipe.id,
|
||||||
title: recipe.title,
|
title: recipe.title,
|
||||||
ingredients: recipe.ingredients,
|
ingredients: recipe.ingredients,
|
||||||
generatedRecipe: recipe.generatedRecipe,
|
preparationTime: recipe.preparationTime,
|
||||||
|
cookingTime: recipe.cookingTime,
|
||||||
|
servings: recipe.servings,
|
||||||
|
difficulty: recipe.difficulty,
|
||||||
|
steps: recipe.steps,
|
||||||
|
tips: recipe.tips,
|
||||||
|
imageUrl: recipe.imageUrl,
|
||||||
createdAt: recipe.createdAt
|
createdAt: recipe.createdAt
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -86,7 +108,11 @@ module.exports = async function (fastify, opts) {
|
|||||||
id: true,
|
id: true,
|
||||||
title: true,
|
title: true,
|
||||||
ingredients: true,
|
ingredients: true,
|
||||||
generatedRecipe: true,
|
preparationTime: true,
|
||||||
|
cookingTime: true,
|
||||||
|
servings: true,
|
||||||
|
difficulty: true,
|
||||||
|
imageUrl: true,
|
||||||
createdAt: true
|
createdAt: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -112,7 +138,13 @@ module.exports = async function (fastify, opts) {
|
|||||||
return reply.code(404).send({ error: 'Recette non trouvée' });
|
return reply.code(404).send({ error: 'Recette non trouvée' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { recipe };
|
// Vous pouvez choisir de parser le JSON ici ou le laisser tel quel
|
||||||
|
const recipeData = {
|
||||||
|
...recipe,
|
||||||
|
generatedRecipe: JSON.parse(recipe.generatedRecipe)
|
||||||
|
};
|
||||||
|
|
||||||
|
return { recipe: recipeData };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
fastify.log.error(error);
|
fastify.log.error(error);
|
||||||
return reply.code(500).send({ error: 'Erreur lors de la récupération de la recette' });
|
return reply.code(500).send({ error: 'Erreur lors de la récupération de la recette' });
|
||||||
|
|||||||
Binary file not shown.
@ -28,9 +28,9 @@ export function Header() {
|
|||||||
const navItems = [
|
const navItems = [
|
||||||
{ name: "Accueil", path: "/", icon: Home, public: true },
|
{ name: "Accueil", path: "/", icon: Home, public: true },
|
||||||
{ name: "Recettes", path: "/recipes", icon: BookOpen, public: true },
|
{ name: "Recettes", path: "/recipes", icon: BookOpen, public: true },
|
||||||
{ name: "Mes recettes", path: "/recipes", icon: BookOpen, public: false },
|
// { name: "Mes recettes", path: "/recipes", icon: BookOpen, public: false },
|
||||||
{ name: "Favoris", path: "/favorites", icon: Heart, public: false },
|
// { name: "Favoris", path: "/favorites", icon: Heart, public: false },
|
||||||
{ name: "Profil", path: "/profile", icon: User, public: false },
|
// { name: "Profil", path: "/profile", icon: User, public: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
const filteredNavItems = navItems.filter((item) => item.public || isAuthenticated)
|
const filteredNavItems = navItems.filter((item) => item.public || isAuthenticated)
|
||||||
|
|||||||
@ -46,22 +46,43 @@ export default function RecipeDetail() {
|
|||||||
// ✅ GET RECIPE DETAILS
|
// ✅ GET RECIPE DETAILS
|
||||||
const recipeData = await recipeService.getRecipeById(id)
|
const recipeData = await recipeService.getRecipeById(id)
|
||||||
|
|
||||||
// Optionnel : conversion ingrédients / instructions si nécessaire
|
// Traiter les données JSON de la recette
|
||||||
let ingredients = recipeData.ingredients
|
let parsedRecipe = recipeData;
|
||||||
if (typeof ingredients === "string") {
|
|
||||||
ingredients = ingredients.split("\n").filter((item) => item.trim() !== "")
|
// Si generatedRecipe est une chaîne JSON, la parser
|
||||||
|
if (typeof recipeData.generatedRecipe === 'string') {
|
||||||
|
try {
|
||||||
|
parsedRecipe.generatedRecipe = JSON.parse(recipeData.generatedRecipe);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("La recette n'est pas au format JSON:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Correction: déclarer la variable instructions
|
// Préparer les ingrédients
|
||||||
let instructions: string[] = []
|
let ingredients = parsedRecipe.ingredients;
|
||||||
if (recipeData.generatedRecipe) {
|
if (typeof ingredients === "string") {
|
||||||
instructions = recipeData.generatedRecipe.split("\n").filter((item) => item.trim() !== "")
|
ingredients = ingredients.split("\n").filter((item) => item.trim() !== "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Préparer les étapes
|
||||||
|
let steps = parsedRecipe.steps || [];
|
||||||
|
if (typeof steps === "string") {
|
||||||
|
steps = steps.split("\n").filter((item) => item.trim() !== "");
|
||||||
|
} else if (parsedRecipe.generatedRecipe && parsedRecipe.generatedRecipe.etapes) {
|
||||||
|
steps = parsedRecipe.generatedRecipe.etapes;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRecipe({
|
setRecipe({
|
||||||
...recipeData,
|
...parsedRecipe,
|
||||||
ingredients,
|
ingredients,
|
||||||
instructions, // Ajouter instructions au recipe
|
instructions: steps,
|
||||||
|
// Utiliser les données structurées si disponibles
|
||||||
|
title: parsedRecipe.title || (parsedRecipe.generatedRecipe?.titre || parsedRecipe.title),
|
||||||
|
preparationTime: parsedRecipe.preparationTime || parsedRecipe.generatedRecipe?.temps_preparation,
|
||||||
|
cookingTime: parsedRecipe.cookingTime || parsedRecipe.generatedRecipe?.temps_cuisson,
|
||||||
|
servings: parsedRecipe.servings || parsedRecipe.generatedRecipe?.portions,
|
||||||
|
difficulty: parsedRecipe.difficulty || parsedRecipe.generatedRecipe?.difficulte,
|
||||||
|
tips: parsedRecipe.tips || parsedRecipe.generatedRecipe?.conseils,
|
||||||
})
|
})
|
||||||
|
|
||||||
// ✅ GET FAVORITE RECIPES & CHECK IF FAVORITE
|
// ✅ GET FAVORITE RECIPES & CHECK IF FAVORITE
|
||||||
@ -397,6 +418,17 @@ export default function RecipeDetail() {
|
|||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
{/* Conseils du chef */}
|
||||||
|
{recipe.tips && (
|
||||||
|
<div className="mt-8 p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg">
|
||||||
|
<h3 className="text-lg font-semibold mb-2 flex items-center">
|
||||||
|
<ChefHat className="h-5 w-5 mr-2 text-amber-600 dark:text-amber-400" />
|
||||||
|
Conseils du chef
|
||||||
|
</h3>
|
||||||
|
<p className="text-amber-800 dark:text-amber-300">{recipe.tips}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -27,7 +27,24 @@ export default function RecipeList() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const data = await recipeService.getRecipes()
|
const data = await recipeService.getRecipes()
|
||||||
setRecipes(data)
|
|
||||||
|
// Traiter les données pour s'assurer que les propriétés sont correctement formatées
|
||||||
|
const processedData = data.map(recipe => {
|
||||||
|
// Convertir les difficultés pour correspondre aux filtres
|
||||||
|
let normalizedDifficulty = recipe.difficulty;
|
||||||
|
if (recipe.difficulty === "facile") normalizedDifficulty = "Facile";
|
||||||
|
if (recipe.difficulty === "moyen") normalizedDifficulty = "Moyen";
|
||||||
|
if (recipe.difficulty === "difficile") normalizedDifficulty = "Difficile";
|
||||||
|
|
||||||
|
return {
|
||||||
|
...recipe,
|
||||||
|
difficulty: normalizedDifficulty,
|
||||||
|
// Ajouter des tags par défaut si nécessaire
|
||||||
|
tags: recipe.tags || []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setRecipes(processedData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Impossible de charger les recettes")
|
setError("Impossible de charger les recettes")
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@ -88,14 +105,14 @@ export default function RecipeList() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col mx-4 md:mx-4 sm:flex-row sm:items-center sm:justify-between mb-6 mt-6">
|
<div className="flex flex-col mx-4 md:mx-4 sm:flex-row sm:items-center sm:justify-between mb-6 mt-6">
|
||||||
{/* <Tabs defaultValue="all" className="w-full sm:w-auto" onValueChange={setActiveFilter}>
|
<Tabs defaultValue="all" className="w-full sm:w-auto" onValueChange={setActiveFilter}>
|
||||||
<TabsList className="grid grid-cols-4 w-full sm:w-auto">
|
<TabsList className="grid grid-cols-4 w-full sm:w-auto">
|
||||||
<TabsTrigger value="all">Toutes</TabsTrigger>
|
<TabsTrigger value="all">Toutes</TabsTrigger>
|
||||||
<TabsTrigger value="easy">Faciles</TabsTrigger>
|
<TabsTrigger value="easy">Faciles</TabsTrigger>
|
||||||
<TabsTrigger value="quick">Rapides</TabsTrigger>
|
<TabsTrigger value="quick">Rapides</TabsTrigger>
|
||||||
<TabsTrigger value="vegetarian">Végé</TabsTrigger>
|
<TabsTrigger value="vegetarian">Végé</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs> */}
|
</Tabs>
|
||||||
{
|
{
|
||||||
filteredRecipes.length !== 0 &&
|
filteredRecipes.length !== 0 &&
|
||||||
<Button
|
<Button
|
||||||
@ -153,6 +170,16 @@ interface RecipeCardProps {
|
|||||||
function RecipeCard({ recipe, index }: RecipeCardProps) {
|
function RecipeCard({ recipe, index }: RecipeCardProps) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
// Normaliser la difficulté pour l'affichage des badges
|
||||||
|
const difficultyClass = {
|
||||||
|
"Facile": "bg-green-500/80 text-white hover:bg-green-600/80",
|
||||||
|
"facile": "bg-green-500/80 text-white hover:bg-green-600/80",
|
||||||
|
"Moyen": "bg-yellow-500/80 text-white hover:bg-yellow-600/80",
|
||||||
|
"moyen": "bg-yellow-500/80 text-white hover:bg-yellow-600/80",
|
||||||
|
"Difficile": "bg-red-500/80 text-white hover:bg-red-600/80",
|
||||||
|
"difficile": "bg-red-500/80 text-white hover:bg-red-600/80"
|
||||||
|
}[recipe.difficulty || ""] || "bg-gray-500/80 text-white hover:bg-gray-600/80";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.05 }}>
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.05 }}>
|
||||||
<Card className="overflow-hidden h-full border-none shadow-md hover:shadow-lg transition-all duration-300">
|
<Card className="overflow-hidden h-full border-none shadow-md hover:shadow-lg transition-all duration-300">
|
||||||
@ -173,19 +200,9 @@ function RecipeCard({ recipe, index }: RecipeCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute top-2 right-2 flex gap-1">
|
<div className="absolute top-2 right-2 flex gap-1">
|
||||||
{recipe.difficulty === "Facile" && (
|
{recipe.difficulty && (
|
||||||
<Badge variant="secondary" className="bg-green-500/80 text-white hover:bg-green-600/80">
|
<Badge variant="secondary" className={difficultyClass}>
|
||||||
Facile
|
{recipe.difficulty.charAt(0).toUpperCase() + recipe.difficulty.slice(1)}
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{recipe.difficulty === "Moyen" && (
|
|
||||||
<Badge variant="secondary" className="bg-yellow-500/80 text-white hover:bg-yellow-600/80">
|
|
||||||
Moyen
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{recipe.difficulty === "Difficile" && (
|
|
||||||
<Badge variant="secondary" className="bg-red-500/80 text-white hover:bg-red-600/80">
|
|
||||||
Difficile
|
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{(recipe.preparationTime || 0) <= 30 && (
|
{(recipe.preparationTime || 0) <= 30 && (
|
||||||
@ -198,7 +215,7 @@ function RecipeCard({ recipe, index }: RecipeCardProps) {
|
|||||||
|
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex flex-wrap gap-1 mb-3">
|
<div className="flex flex-wrap gap-1 mb-3">
|
||||||
{recipe.tags?.slice(0, 3).map((tag) => (
|
{recipe.tags && Array.isArray(recipe.tags) && recipe.tags.slice(0, 3).map((tag) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={tag}
|
key={tag}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -210,12 +227,10 @@ function RecipeCard({ recipe, index }: RecipeCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||||
{recipe.preparationTime && (
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
<Clock className="h-3 w-3 mr-1" />
|
||||||
<Clock className="h-3 w-3 mr-1" />
|
<span>{recipe.preparationTime || 0} min</span>
|
||||||
<span>{recipe.preparationTime} min</span>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{recipe.servings && (
|
{recipe.servings && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|||||||
22
recorder/package-lock.json
generated
22
recorder/package-lock.json
generated
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "recorder",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "recorder",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"vmsg": "^0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vmsg": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vmsg/-/vmsg-0.4.0.tgz",
|
|
||||||
"integrity": "sha512-46BBqRSfqdFGUpO2j+Hpz8T9YE5uWG0/PWal1PT+R1o8NEthtjG/XWl4HzbB8hIHpg/UtmKvsxL2OKQBrIYcHQ==",
|
|
||||||
"license": "CC0-1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "recorder",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"description": "",
|
|
||||||
"dependencies": {
|
|
||||||
"vmsg": "^0.4.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,302 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Enregistreur Audio</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
color: #e67e22;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 20px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recorder-controls {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: #e67e22;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #d35400;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
background-color: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recordings {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recording-item {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recording-info {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
audio {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
text-align: center;
|
|
||||||
margin: 10px 0;
|
|
||||||
font-style: italic;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulse {
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mic-icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<!-- Charger le script vmsg avant notre script principal -->
|
|
||||||
<script src="https://unpkg.com/vmsg@0.3.0/vmsg.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h1>Enregistreur Audio</h1>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<p>Enregistrez votre voix et écoutez le résultat. Les enregistrements sont stockés localement dans votre navigateur.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div id="status" class="status">Prêt à enregistrer</div>
|
|
||||||
|
|
||||||
<div class="recorder-controls">
|
|
||||||
<button id="recordButton">
|
|
||||||
<svg class="mic-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
|
|
||||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
|
||||||
<line x1="12" y1="19" x2="12" y2="23"></line>
|
|
||||||
<line x1="8" y1="23" x2="16" y2="23"></line>
|
|
||||||
</svg>
|
|
||||||
Commencer l'enregistrement
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="recordings" class="recordings">
|
|
||||||
<h3>Vos enregistrements</h3>
|
|
||||||
<div id="recordingsList"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Attendre que vmsg soit complètement chargé
|
|
||||||
window.onload = function () {
|
|
||||||
// Vérifier si vmsg est disponible
|
|
||||||
if (typeof vmsg === 'undefined') {
|
|
||||||
console.error("La bibliothèque vmsg n'a pas été chargée correctement");
|
|
||||||
document.getElementById('status').textContent = "Erreur: Impossible de charger l'enregistreur audio";
|
|
||||||
document.getElementById('recordButton').disabled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialiser le recorder
|
|
||||||
const recorder = new vmsg.Recorder({
|
|
||||||
wasmURL: "https://unpkg.com/vmsg@0.3.0/vmsg.wasm"
|
|
||||||
});
|
|
||||||
|
|
||||||
const recordButton = document.getElementById('recordButton');
|
|
||||||
const statusElement = document.getElementById('status');
|
|
||||||
const recordingsList = document.getElementById('recordingsList');
|
|
||||||
|
|
||||||
let isLoading = false;
|
|
||||||
let isRecording = false;
|
|
||||||
let recordings = [];
|
|
||||||
|
|
||||||
// Fonction pour mettre à jour l'interface utilisateur
|
|
||||||
function updateUI() {
|
|
||||||
if (isLoading) {
|
|
||||||
recordButton.disabled = true;
|
|
||||||
statusElement.textContent = isRecording
|
|
||||||
? "Arrêt de l'enregistrement..."
|
|
||||||
: "Initialisation de l'enregistrement...";
|
|
||||||
} else if (isRecording) {
|
|
||||||
recordButton.innerHTML = `
|
|
||||||
<svg class="mic-icon pulse" viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
|
|
||||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
|
||||||
<line x1="12" y1="19" x2="12" y2="23"></line>
|
|
||||||
<line x1="8" y1="23" x2="16" y2="23"></line>
|
|
||||||
</svg>
|
|
||||||
Arrêter l'enregistrement
|
|
||||||
`;
|
|
||||||
recordButton.style.backgroundColor = '#e74c3c';
|
|
||||||
statusElement.textContent = "Enregistrement en cours...";
|
|
||||||
} else {
|
|
||||||
recordButton.disabled = false;
|
|
||||||
recordButton.innerHTML = `
|
|
||||||
<svg class="mic-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
|
|
||||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
|
||||||
<line x1="12" y1="19" x2="12" y2="23"></line>
|
|
||||||
<line x1="8" y1="23" x2="16" y2="23"></line>
|
|
||||||
</svg>
|
|
||||||
Commencer l'enregistrement
|
|
||||||
`;
|
|
||||||
recordButton.style.backgroundColor = '#e67e22';
|
|
||||||
statusElement.textContent = "Prêt à enregistrer";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour gérer l'enregistrement
|
|
||||||
async function toggleRecording() {
|
|
||||||
isLoading = true;
|
|
||||||
updateUI();
|
|
||||||
|
|
||||||
if (isRecording) {
|
|
||||||
try {
|
|
||||||
const blob = await recorder.stopRecording();
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const timestamp = new Date().toLocaleString();
|
|
||||||
const size = (blob.size / 1024).toFixed(2);
|
|
||||||
|
|
||||||
recordings.push({ url, timestamp, size });
|
|
||||||
renderRecordings();
|
|
||||||
|
|
||||||
isRecording = false;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Erreur lors de l'arrêt de l'enregistrement:", e);
|
|
||||||
alert("Une erreur est survenue lors de l'arrêt de l'enregistrement.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await recorder.initAudio();
|
|
||||||
await recorder.initWorker();
|
|
||||||
recorder.startRecording();
|
|
||||||
isRecording = true;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Erreur lors du démarrage de l'enregistrement:", e);
|
|
||||||
alert("Impossible d'accéder au microphone. Vérifiez les permissions.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading = false;
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour afficher les enregistrements
|
|
||||||
function renderRecordings() {
|
|
||||||
recordingsList.innerHTML = '';
|
|
||||||
|
|
||||||
if (recordings.length === 0) {
|
|
||||||
recordingsList.innerHTML = '<p>Aucun enregistrement pour le moment.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
recordings.forEach((recording, index) => {
|
|
||||||
const recordingItem = document.createElement('div');
|
|
||||||
recordingItem.className = 'recording-item';
|
|
||||||
|
|
||||||
recordingItem.innerHTML = `
|
|
||||||
<div class="recording-info">
|
|
||||||
<strong>Enregistrement #${index + 1}</strong> - ${recording.timestamp} (${recording.size} KB)
|
|
||||||
</div>
|
|
||||||
<audio controls src="${recording.url}"></audio>
|
|
||||||
<div style="margin-top: 10px;">
|
|
||||||
<button class="download-btn" data-index="${index}" style="background-color: #3498db;">
|
|
||||||
Télécharger
|
|
||||||
</button>
|
|
||||||
<button class="delete-btn" data-index="${index}" style="background-color: #e74c3c; margin-left: 10px;">
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
recordingsList.appendChild(recordingItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ajouter les écouteurs d'événements pour les boutons
|
|
||||||
document.querySelectorAll('.download-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function () {
|
|
||||||
const index = parseInt(this.dataset.index);
|
|
||||||
const recording = recordings[index];
|
|
||||||
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = recording.url;
|
|
||||||
a.download = `enregistrement-${index + 1}.mp3`;
|
|
||||||
a.click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.delete-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function () {
|
|
||||||
const index = parseInt(this.dataset.index);
|
|
||||||
URL.revokeObjectURL(recordings[index].url);
|
|
||||||
recordings.splice(index, 1);
|
|
||||||
renderRecordings();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialiser l'interface
|
|
||||||
renderRecordings();
|
|
||||||
updateUI();
|
|
||||||
|
|
||||||
// Ajouter l'écouteur d'événement pour le bouton d'enregistrement
|
|
||||||
recordButton.addEventListener('click', toggleRecording);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user