add text to audio

tst2
suvodip ghosh 2025-06-06 08:34:20 +00:00
parent 6071cd5228
commit b8e20ab510
2 changed files with 277 additions and 1 deletions

View File

@ -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">

View File

@ -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">
&#9654; <!-- 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">
&#10074;&#10074; <!-- 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">
&#9654; <!-- 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">
&#9632; <!-- 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>