add text to audio
parent
6071cd5228
commit
b8e20ab510
|
@ -59,7 +59,7 @@ export function ContactForm() {
|
|||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6" id='contact-form'>
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{/* Name and Email */}
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
|
||||
|
|
|
@ -0,0 +1,276 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro"
|
||||
---
|
||||
<Layout title="">
|
||||
<div class="bg-gradient-to-br from-indigo-50 to-blue-100 min-h-screen">
|
||||
<div class="container mx-auto px-4 py-12">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-10">
|
||||
<h1 class="text-4xl font-bold text-indigo-800 mb-2">VoiceCraft Pro</h1>
|
||||
<p class="text-lg text-indigo-600">Convert text to natural sounding speech</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Card -->
|
||||
<div class="bg-white rounded-xl shadow-xl overflow-hidden">
|
||||
<!-- Text Area -->
|
||||
<div class="p-6 border-b border-gray-200">
|
||||
<label for="text-to-speak" class="block text-sm font-medium text-gray-700 mb-2">Enter your text</label>
|
||||
<textarea id="text-to-speak" rows="6" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" placeholder="Type or paste your text here..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="p-6 border-b border-gray-200">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Voice Selection -->
|
||||
<div>
|
||||
<label for="voice-select" class="block text-sm font-medium text-gray-700 mb-2">Select Voice</label>
|
||||
<select id="voice-select" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition">
|
||||
<option value="">Loading voices...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Pitch Control -->
|
||||
<div>
|
||||
<label for="pitch" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<svg class="icon inline mr-1" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.31-8.86c-1.77-.45-2.34-.94-2.34-1.67 0-.84.79-1.43 2.1-1.43 1.38 0 1.9.66 1.94 1.64h1.71c-.05-1.34-.87-2.57-2.49-2.97V5H10.9v1.69c-1.51.32-2.72 1.3-2.72 2.81 0 1.79 1.49 2.69 3.66 3.21 1.95.46 2.34 1.15 2.34 1.87 0 .53-.39 1.39-2.1 1.39-1.6 0-2.23-.72-2.32-1.64H8.04c.1 1.7 1.36 2.66 2.86 2.97V19h2.34v-1.67c1.52-.29 2.72-1.16 2.73-2.77-.01-2.2-1.9-2.96-3.66-3.42z"/>
|
||||
</svg>
|
||||
Pitch: <span id="pitch-value">1</span>
|
||||
</label>
|
||||
<input type="range" id="pitch" min="0.5" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
|
||||
</div>
|
||||
|
||||
<!-- Rate Control -->
|
||||
<div>
|
||||
<label for="rate" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<svg class="icon inline mr-1" viewBox="0 0 24 24">
|
||||
<path d="M13.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zM9.8 8.9L7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6l1.8-.7"/>
|
||||
</svg>
|
||||
Speed: <span id="rate-value">1</span>
|
||||
</label>
|
||||
<input type="range" id="rate" min="0.5" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
|
||||
</div>
|
||||
|
||||
<!-- Volume Control -->
|
||||
<div>
|
||||
<label for="volume" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<svg class="icon inline mr-1" viewBox="0 0 24 24">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||
</svg>
|
||||
Volume: <span id="volume-value">1</span>
|
||||
</label>
|
||||
<input type="range" id="volume" min="0" max="1" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="p-6 flex flex-wrap justify-between items-center gap-4">
|
||||
<div class="flex gap-3">
|
||||
<button id="speak-btn" class="px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-medium transition flex items-center gap-2">
|
||||
▶ <!-- Play symbol --> Speak
|
||||
</button>
|
||||
<button id="pause-btn" class="px-6 py-3 bg-amber-500 hover:bg-amber-600 text-white rounded-lg font-medium transition flex items-center gap-2">
|
||||
❚❚ <!-- Pause symbol --> Pause
|
||||
</button>
|
||||
<button id="resume-btn" class="px-6 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg font-medium transition flex items-center gap-2">
|
||||
▶ <!-- Play symbol --> Resume
|
||||
</button>
|
||||
<button id="stop-btn" class="px-6 py-3 bg-red-500 hover:bg-red-600 text-white rounded-lg font-medium transition flex items-center gap-2">
|
||||
■ <!-- Stop symbol --> Stop
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button id="download-btn" class="px-6 py-3 bg-gray-800 hover:bg-gray-900 text-white rounded-lg font-medium transition flex items-center gap-2">
|
||||
<svg class="icon" viewBox="0 0 24 24">
|
||||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||||
</svg>
|
||||
Download Audio
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div id="status" class="mt-4 text-center text-gray-500 text-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script is:inline>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// DOM Elements
|
||||
const textInput = document.getElementById('text-to-speak');
|
||||
const voiceSelect = document.getElementById('voice-select');
|
||||
const pitchInput = document.getElementById('pitch');
|
||||
const rateInput = document.getElementById('rate');
|
||||
const volumeInput = document.getElementById('volume');
|
||||
const pitchValue = document.getElementById('pitch-value');
|
||||
const rateValue = document.getElementById('rate-value');
|
||||
const volumeValue = document.getElementById('volume-value');
|
||||
const speakBtn = document.getElementById('speak-btn');
|
||||
const pauseBtn = document.getElementById('pause-btn');
|
||||
const resumeBtn = document.getElementById('resume-btn');
|
||||
const stopBtn = document.getElementById('stop-btn');
|
||||
const downloadBtn = document.getElementById('download-btn');
|
||||
const statusEl = document.getElementById('status');
|
||||
|
||||
// Speech synthesis
|
||||
const synth = window.speechSynthesis;
|
||||
let voices = [];
|
||||
let utterance = null;
|
||||
let audioChunks = [];
|
||||
let mediaRecorder;
|
||||
let audioBlob;
|
||||
|
||||
// Update display values
|
||||
pitchInput.addEventListener('input', () => pitchValue.textContent = pitchInput.value);
|
||||
rateInput.addEventListener('input', () => rateValue.textContent = rateInput.value);
|
||||
volumeInput.addEventListener('input', () => volumeValue.textContent = volumeInput.value);
|
||||
|
||||
// Load voices
|
||||
function loadVoices() {
|
||||
voices = synth.getVoices();
|
||||
voiceSelect.innerHTML = '';
|
||||
|
||||
voices.forEach(voice => {
|
||||
const option = document.createElement('option');
|
||||
option.textContent = `${voice.name} (${voice.lang})${voice.default ? ' — DEFAULT' : ''}`;
|
||||
option.setAttribute('data-name', voice.name);
|
||||
option.setAttribute('data-lang', voice.lang);
|
||||
voiceSelect.appendChild(option);
|
||||
});
|
||||
|
||||
if (voices.length === 0) {
|
||||
statusEl.textContent = 'No voices available. Please try again later.';
|
||||
} else {
|
||||
statusEl.textContent = `${voices.length} voices loaded.`;
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome needs this
|
||||
if (speechSynthesis.onvoiceschanged !== undefined) {
|
||||
speechSynthesis.onvoiceschanged = loadVoices;
|
||||
}
|
||||
|
||||
// Initialize
|
||||
loadVoices();
|
||||
|
||||
// Speak function
|
||||
function speak() {
|
||||
if (synth.speaking) {
|
||||
synth.cancel();
|
||||
}
|
||||
|
||||
if (textInput.value.trim() === '') {
|
||||
statusEl.textContent = 'Please enter some text first.';
|
||||
return;
|
||||
}
|
||||
|
||||
utterance = new SpeechSynthesisUtterance(textInput.value);
|
||||
|
||||
const selectedOption = voiceSelect.selectedOptions[0];
|
||||
if (selectedOption) {
|
||||
const selectedVoice = voices.find(voice =>
|
||||
voice.name === selectedOption.getAttribute('data-name') &&
|
||||
voice.lang === selectedOption.getAttribute('data-lang')
|
||||
);
|
||||
if (selectedVoice) {
|
||||
utterance.voice = selectedVoice;
|
||||
}
|
||||
}
|
||||
|
||||
utterance.pitch = parseFloat(pitchInput.value);
|
||||
utterance.rate = parseFloat(rateInput.value);
|
||||
utterance.volume = parseFloat(volumeInput.value);
|
||||
|
||||
// Set up MediaRecorder for audio capture
|
||||
setupAudioCapture();
|
||||
|
||||
utterance.onstart = () => {
|
||||
statusEl.textContent = 'Speaking...';
|
||||
mediaRecorder.start();
|
||||
};
|
||||
|
||||
utterance.onend = () => {
|
||||
statusEl.textContent = 'Speech finished.';
|
||||
mediaRecorder.stop();
|
||||
};
|
||||
|
||||
utterance.onerror = (event) => {
|
||||
statusEl.textContent = `Error occurred: ${event.error}`;
|
||||
};
|
||||
|
||||
synth.speak(utterance);
|
||||
}
|
||||
|
||||
// Set up audio capture for download
|
||||
function setupAudioCapture() {
|
||||
audioChunks = [];
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const destination = audioContext.createMediaStreamDestination();
|
||||
mediaRecorder = new MediaRecorder(destination.stream);
|
||||
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
if (event.data.size > 0) {
|
||||
audioChunks.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = () => {
|
||||
audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
||||
};
|
||||
}
|
||||
|
||||
// Button event listeners
|
||||
speakBtn.addEventListener('click', speak);
|
||||
|
||||
pauseBtn.addEventListener('click', () => {
|
||||
if (synth.speaking) {
|
||||
synth.pause();
|
||||
statusEl.textContent = 'Speech paused.';
|
||||
}
|
||||
});
|
||||
|
||||
resumeBtn.addEventListener('click', () => {
|
||||
if (synth.paused) {
|
||||
synth.resume();
|
||||
statusEl.textContent = 'Resuming speech...';
|
||||
}
|
||||
});
|
||||
|
||||
stopBtn.addEventListener('click', () => {
|
||||
if (synth.speaking || synth.paused) {
|
||||
synth.cancel();
|
||||
statusEl.textContent = 'Speech stopped.';
|
||||
}
|
||||
});
|
||||
|
||||
downloadBtn.addEventListener('click', () => {
|
||||
if (!audioBlob) {
|
||||
statusEl.textContent = 'No audio to download. Please speak first.';
|
||||
return;
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(audioBlob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'speech-output.wav';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
statusEl.textContent = 'Audio downloaded successfully.';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.125em;
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue