Spaces:
Running
Running
<html lang="fr"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Transcription Audio avec GPT-4</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://unpkg.com/dropzone@5/dist/min/dropzone.min.js"></script> | |
<link rel="stylesheet" href="https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" type="text/css" /> | |
<style> | |
.dropzone { | |
border: 2px dashed #3b82f6; | |
border-radius: 0.5rem; | |
background: #f8fafc; | |
min-height: 200px; | |
padding: 20px; | |
} | |
.dropzone .dz-message { | |
font-size: 1.25rem; | |
color: #64748b; | |
} | |
.progress-bar { | |
height: 6px; | |
background-color: #e2e8f0; | |
border-radius: 3px; | |
margin-top: 10px; | |
} | |
.progress-bar-fill { | |
height: 100%; | |
border-radius: 3px; | |
background-color: #3b82f6; | |
width: 0%; | |
transition: width 0.3s ease; | |
} | |
.file-item { | |
transition: all 0.3s ease; | |
} | |
.file-item:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
} | |
.loader { | |
border: 3px solid #f3f3f3; | |
border-top: 3px solid #3b82f6; | |
border-radius: 50%; | |
width: 20px; | |
height: 20px; | |
animation: spin 1s linear infinite; | |
display: inline-block; | |
vertical-align: middle; | |
margin-right: 8px; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50 min-h-screen"> | |
<div class="container mx-auto px-4 py-8 max-w-4xl"> | |
<div class="text-center mb-8"> | |
<h1 class="text-3xl font-bold text-gray-800 mb-2">Transcription Audio avec GPT-4</h1> | |
<p class="text-gray-600">Transcrivez vos fichiers audio en texte avec une précision exceptionnelle</p> | |
</div> | |
<div class="bg-white rounded-xl shadow-md p-6 mb-8"> | |
<div class="mb-6"> | |
<label for="api-key" class="block text-sm font-medium text-gray-700 mb-1">Clé API OpenAI</label> | |
<div class="flex"> | |
<input type="password" id="api-key" placeholder="sk-..." | |
class="flex-1 px-4 py-2 border border-gray-300 rounded-l-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
<button id="toggle-key" class="px-4 py-2 bg-gray-100 border border-l-0 border-gray-300 rounded-r-md hover:bg-gray-200"> | |
👁️ | |
</button> | |
</div> | |
<p class="mt-1 text-xs text-gray-500">Votre clé API n'est jamais envoyée à nos serveurs</p> | |
</div> | |
<div class="mb-4"> | |
<label class="block text-sm font-medium text-gray-700 mb-2">Options de transcription</label> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
<div> | |
<label for="language" class="block text-xs text-gray-500 mb-1">Langue</label> | |
<select id="language" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
<option value="fr">Français</option> | |
<option value="en">Anglais</option> | |
<option value="es">Espagnol</option> | |
<option value="de">Allemand</option> | |
<option value="it">Italien</option> | |
</select> | |
</div> | |
<div> | |
<label for="model" class="block text-xs text-gray-500 mb-1">Modèle</label> | |
<select id="model" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
<option value="whisper-1">Whisper (optimisé pour audio)</option> | |
<option value="gpt-4o">GPT-4 Omni (meilleure précision)</option> | |
</select> | |
</div> | |
</div> | |
</div> | |
<div id="dropzone" class="dropzone"> | |
<div class="dz-message" data-dz-message> | |
<div class="flex flex-col items-center justify-center"> | |
<svg class="w-12 h-12 text-blue-500 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path> | |
</svg> | |
<span>Glissez-déposez vos fichiers audio ici</span> | |
<span class="text-sm text-gray-500 mt-1">ou cliquez pour sélectionner</span> | |
</div> | |
</div> | |
</div> | |
<div class="mt-4"> | |
<div class="flex justify-between items-center mb-2"> | |
<span class="text-sm font-medium text-gray-700">Fichiers en attente</span> | |
<button id="transcribe-all" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" disabled> | |
Transcrire tout | |
</button> | |
</div> | |
<div id="file-list" class="space-y-2"> | |
<!-- Les fichiers apparaîtront ici --> | |
</div> | |
</div> | |
</div> | |
<div id="results" class="bg-white rounded-xl shadow-md p-6 hidden"> | |
<div class="flex justify-between items-center mb-4"> | |
<h2 class="text-xl font-semibold text-gray-800">Résultats de transcription</h2> | |
<button id="copy-all" class="px-3 py-1 bg-gray-100 text-gray-700 rounded-md text-sm hover:bg-gray-200"> | |
Copier tout | |
</button> | |
</div> | |
<div id="transcription-results" class="space-y-4"> | |
<!-- Les résultats apparaîtront ici --> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// Toggle API key visibility | |
const apiKeyInput = document.getElementById('api-key'); | |
const toggleKeyBtn = document.getElementById('toggle-key'); | |
toggleKeyBtn.addEventListener('click', function() { | |
if (apiKeyInput.type === 'password') { | |
apiKeyInput.type = 'text'; | |
toggleKeyBtn.textContent = '🔒'; | |
} else { | |
apiKeyInput.type = 'password'; | |
toggleKeyBtn.textContent = '👁️'; | |
} | |
}); | |
// Initialize Dropzone | |
Dropzone.autoDiscover = false; | |
const myDropzone = new Dropzone("#dropzone", { | |
url: "/fake-url", // We'll handle the upload manually | |
paramName: "file", | |
maxFilesize: 50, // MB | |
acceptedFiles: "audio/*,video/*,.mp3,.wav,.m4a,.mp4,.ogg", | |
addRemoveLinks: true, | |
autoProcessQueue: false, | |
dictDefaultMessage: "", | |
dictFallbackMessage: "Votre navigateur ne supporte pas le glisser-déposer de fichiers.", | |
dictFileTooBig: "Fichier trop volumineux ({{filesize}}MB). Taille maximale: {{maxFilesize}}MB.", | |
dictInvalidFileType: "Type de fichier non supporté.", | |
dictResponseError: "Le serveur a répondu avec le code {{statusCode}}.", | |
dictCancelUpload: "Annuler", | |
dictUploadCanceled: "Téléchargement annulé.", | |
dictRemoveFile: "Supprimer", | |
dictMaxFilesExceeded: "Vous ne pouvez pas télécharger plus de fichiers." | |
}); | |
const fileList = document.getElementById('file-list'); | |
const transcribeAllBtn = document.getElementById('transcribe-all'); | |
const resultsSection = document.getElementById('results'); | |
const transcriptionResults = document.getElementById('transcription-results'); | |
const copyAllBtn = document.getElementById('copy-all'); | |
let filesToTranscribe = []; | |
// Handle added files | |
myDropzone.on("addedfile", function(file) { | |
filesToTranscribe.push(file); | |
updateFileList(); | |
transcribeAllBtn.disabled = filesToTranscribe.length === 0; | |
}); | |
// Handle removed files | |
myDropzone.on("removedfile", function(file) { | |
filesToTranscribe = filesToTranscribe.filter(f => f !== file); | |
updateFileList(); | |
transcribeAllBtn.disabled = filesToTranscribe.length === 0; | |
// Remove from DOM if exists | |
const fileElement = document.getElementById(`file-${file.upload.uuid}`); | |
if (fileElement) { | |
fileElement.remove(); | |
} | |
}); | |
// Update the file list UI | |
function updateFileList() { | |
fileList.innerHTML = ''; | |
filesToTranscribe.forEach(file => { | |
const fileElement = document.createElement('div'); | |
fileElement.id = `file-${file.upload.uuid}`; | |
fileElement.className = 'file-item bg-gray-50 rounded-lg p-3 flex justify-between items-center'; | |
fileElement.innerHTML = ` | |
<div class="flex items-center"> | |
<svg class="w-5 h-5 text-blue-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"></path> | |
</svg> | |
<span class="text-sm font-medium">${file.name}</span> | |
<span class="text-xs text-gray-500 ml-2">(${(file.size / (1024 * 1024)).toFixed(2)} MB)</span> | |
</div> | |
<div class="flex items-center"> | |
<button class="transcribe-btn px-3 py-1 bg-blue-600 text-white rounded-md text-sm hover:bg-blue-700 mr-2" data-file-id="${file.upload.uuid}"> | |
Transcrire | |
</button> | |
<button class="remove-btn text-gray-500 hover:text-red-500" data-file-id="${file.upload.uuid}"> | |
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path> | |
</svg> | |
</button> | |
</div> | |
`; | |
fileList.appendChild(fileElement); | |
// Add event listeners to the buttons | |
fileElement.querySelector('.transcribe-btn').addEventListener('click', () => transcribeFile(file)); | |
fileElement.querySelector('.remove-btn').addEventListener('click', () => { | |
myDropzone.removeFile(file); | |
}); | |
}); | |
} | |
// Transcribe a single file | |
async function transcribeFile(file) { | |
const apiKey = apiKeyInput.value.trim(); | |
if (!apiKey) { | |
alert('Veuillez entrer votre clé API OpenAI'); | |
return; | |
} | |
const language = document.getElementById('language').value; | |
const model = document.getElementById('model').value; | |
const fileElement = document.getElementById(`file-${file.upload.uuid}`); | |
if (!fileElement) return; | |
const transcribeBtn = fileElement.querySelector('.transcribe-btn'); | |
transcribeBtn.disabled = true; | |
transcribeBtn.innerHTML = '<span class="loader"></span> Traitement...'; | |
try { | |
// In a real app, you would upload the file to your server first | |
// Then your server would call the OpenAI API | |
// Here we're simulating the process with a timeout | |
// Simulate upload progress | |
let progress = 0; | |
const progressInterval = setInterval(() => { | |
progress += Math.random() * 10; | |
if (progress >= 90) clearInterval(progressInterval); | |
transcribeBtn.innerHTML = `<span class="loader"></span> Upload ${Math.min(100, Math.round(progress))}%`; | |
}, 300); | |
// Simulate API call to OpenAI | |
const response = await simulateOpenAICall(apiKey, file, language, model); | |
clearInterval(progressInterval); | |
transcribeBtn.innerHTML = 'Terminé ✓'; | |
// Show results | |
showTranscriptionResult(file.name, response); | |
// Remove file from list after a delay | |
setTimeout(() => { | |
myDropzone.removeFile(file); | |
}, 2000); | |
} catch (error) { | |
console.error('Error:', error); | |
transcribeBtn.innerHTML = 'Erreur'; | |
alert('Une erreur est survenue lors de la transcription: ' + error.message); | |
} | |
} | |
// Simulate OpenAI API call (replace with actual API call in production) | |
function simulateOpenAICall(apiKey, file, language, model) { | |
return new Promise((resolve) => { | |
setTimeout(() => { | |
// This is a mock response - in a real app you would call: | |
// const formData = new FormData(); | |
// formData.append('file', file); | |
// formData.append('model', 'whisper-1'); | |
// formData.append('language', language); | |
// const response = await fetch('https://api.openai.com/v1/audio/transcriptions', { | |
// method: 'POST', | |
// headers: { | |
// 'Authorization': `Bearer ${apiKey}` | |
// }, | |
// body: formData | |
// }); | |
// const data = await response.json(); | |
// Mock data based on file name | |
const mockText = `Ceci est une transcription simulée du fichier ${file.name}. | |
Les systèmes de transcription automatique comme Whisper et GPT-4 offrent des résultats de plus en plus précis. Cette démo montre l'interface utilisateur, mais dans une application réelle, vous appelleriez l'API OpenAI pour obtenir une vraie transcription. | |
La qualité de la transcription dépend de plusieurs facteurs : | |
- La qualité de l'enregistrement audio | |
- La présence de bruit de fond | |
- La complexité du vocabulaire utilisé | |
- L'accent des locuteurs | |
Pour une meilleure précision, utilisez des fichiers audio de haute qualité et spécifiez la langue correcte.`; | |
resolve({ text: mockText }); | |
}, 3000); | |
}); | |
} | |
// Show transcription result | |
function showTranscriptionResult(filename, result) { | |
resultsSection.classList.remove('hidden'); | |
const resultElement = document.createElement('div'); | |
resultElement.className = 'bg-gray-50 rounded-lg p-4'; | |
resultElement.innerHTML = ` | |
<div class="flex justify-between items-center mb-2"> | |
<h3 class="font-medium text-blue-600">${filename}</h3> | |
<button class="copy-btn px-2 py-1 bg-gray-200 text-gray-700 rounded text-xs hover:bg-gray-300 flex items-center"> | |
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path> | |
</svg> | |
Copier | |
</button> | |
</div> | |
<div class="text-sm text-gray-700 whitespace-pre-line transcription-text">${result.text}</div> | |
`; | |
transcriptionResults.appendChild(resultElement); | |
// Add copy button functionality | |
resultElement.querySelector('.copy-btn').addEventListener('click', () => { | |
const textToCopy = resultElement.querySelector('.transcription-text').textContent; | |
navigator.clipboard.writeText(textToCopy).then(() => { | |
const btn = resultElement.querySelector('.copy-btn'); | |
btn.innerHTML = '<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg> Copié!'; | |
setTimeout(() => { | |
btn.innerHTML = '<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path></svg> Copier'; | |
}, 2000); | |
}); | |
}); | |
} | |
// Transcribe all files | |
transcribeAllBtn.addEventListener('click', async () => { | |
const apiKey = apiKeyInput.value.trim(); | |
if (!apiKey) { | |
alert('Veuillez entrer votre clé API OpenAI'); | |
return; | |
} | |
transcribeAllBtn.disabled = true; | |
transcribeAllBtn.innerHTML = '<span class="loader"></span> Traitement...'; | |
// Process files sequentially | |
for (const file of [...filesToTranscribe]) { | |
await transcribeFile(file); | |
} | |
transcribeAllBtn.innerHTML = 'Tout transcrire'; | |
}); | |
// Copy all transcriptions | |
copyAllBtn.addEventListener('click', () => { | |
const allTexts = Array.from(document.querySelectorAll('.transcription-text')) | |
.map(el => el.textContent) | |
.join('\n\n'); | |
navigator.clipboard.writeText(allTexts).then(() => { | |
copyAllBtn.textContent = 'Tout copié!'; | |
setTimeout(() => { | |
copyAllBtn.textContent = 'Copier tout'; | |
}, 2000); | |
}); | |
}); | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=EGuihaire/paris-space" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |