first commit

This commit is contained in:
2025-04-04 13:23:15 -06:00
commit 216064f731
2103 changed files with 522593 additions and 0 deletions

236
static/css/style.css Normal file
View File

@@ -0,0 +1,236 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
margin-bottom: 5px;
color: #2c3e50;
}
.subtitle {
text-align: center;
color: #7f8c8d;
margin-bottom: 30px;
font-size: 0.9rem;
}
.translation-panel {
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
}
.language-selector {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.select-container {
flex: 1;
min-width: 120px;
margin: 5px;
}
.select-container label {
display: block;
margin-bottom: 5px;
font-size: 0.9rem;
color: #7f8c8d;
}
select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
background-color: #f9f9f9;
font-size: 1rem;
}
#swapLanguages {
background-color: #e8f4fc;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
cursor: pointer;
transition: background-color 0.3s;
margin: 0 10px;
}
#swapLanguages:hover {
background-color: #d1e9f9;
}
.text-panels {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 20px;
}
.text-panel {
flex: 1;
}
textarea {
width: 100%;
height: 120px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 1rem;
resize: none;
margin-bottom: 10px;
}
.controls {
display: flex;
gap: 10px;
}
button {
padding: 10px 15px;
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.3s, transform 0.1s;
}
button:active {
transform: translateY(1px);
}
.record-button, .speak-button {
background-color: #e8f4fc;
color: #3498db;
display: flex;
align-items: center;
gap: 5px;
}
.record-button:hover, .speak-button:hover {
background-color: #d1e9f9;
}
.record-button.recording {
background-color: #ffe9e9;
color: #e74c3c;
animation: pulse 1.5s infinite;
}
.clear-button, .copy-button {
background-color: #f5f5f5;
color: #7f8c8d;
}
.clear-button:hover, .copy-button:hover {
background-color: #e9e9e9;
}
.primary-button {
width: 100%;
padding: 15px;
background-color: #3498db;
color: white;
font-size: 1rem;
font-weight: 500;
border-radius: 8px;
}
.primary-button:hover {
background-color: #2980b9;
}
.status-message {
text-align: center;
min-height: 24px;
color: #7f8c8d;
font-size: 0.9rem;
}
.status-message.error {
color: #e74c3c;
}
.status-message.success {
color: #2ecc71;
}
/* Animation for recording */
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
/* Media queries for responsiveness */
@media (min-width: 768px) {
.text-panels {
flex-direction: row;
}
textarea {
height: 150px;
}
}
@media (max-width: 480px) {
.container {
padding: 10px;
}
h1 {
font-size: 1.5rem;
}
.translation-panel {
padding: 15px;
}
textarea {
height: 100px;
}
.button-text {
display: none;
}
.record-button, .speak-button, .clear-button, .copy-button {
padding: 10px;
flex: 1;
justify-content: center;
}
}

255
static/js/main.js Normal file
View File

@@ -0,0 +1,255 @@
document.addEventListener('DOMContentLoaded', function() {
// DOM elements
const sourceLanguage = document.getElementById('sourceLanguage');
const targetLanguage = document.getElementById('targetLanguage');
const swapButton = document.getElementById('swapLanguages');
const sourceText = document.getElementById('sourceText');
const translatedText = document.getElementById('translatedText');
const recordSourceButton = document.getElementById('recordSource');
const speakButton = document.getElementById('speak');
const clearSourceButton = document.getElementById('clearSource');
const copyTranslationButton = document.getElementById('copyTranslation');
const translateButton = document.getElementById('translateButton');
const statusMessage = document.getElementById('status');
// Audio recording variables
let mediaRecorder;
let audioChunks = [];
let isRecording = false;
// Speech recognition setup
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = SpeechRecognition ? new SpeechRecognition() : null;
if (recognition) {
recognition.continuous = false;
recognition.interimResults = false;
}
// Event listeners
swapButton.addEventListener('click', swapLanguages);
translateButton.addEventListener('click', translateText);
clearSourceButton.addEventListener('click', clearSource);
copyTranslationButton.addEventListener('click', copyTranslation);
if (recognition) {
recordSourceButton.addEventListener('click', toggleRecording);
} else {
recordSourceButton.textContent = "Speech API not supported";
recordSourceButton.disabled = true;
}
speakButton.addEventListener('click', speakTranslation);
// Functions (continued)
function swapLanguages() {
const tempLang = sourceLanguage.value;
sourceLanguage.value = targetLanguage.value;
targetLanguage.value = tempLang;
// Also swap the text if both fields have content
if (sourceText.value && translatedText.value) {
const tempText = sourceText.value;
sourceText.value = translatedText.value;
translatedText.value = tempText;
}
}
function clearSource() {
sourceText.value = '';
updateStatus('');
}
function copyTranslation() {
if (!translatedText.value) {
updateStatus('Nothing to copy', 'error');
return;
}
navigator.clipboard.writeText(translatedText.value)
.then(() => {
updateStatus('Copied to clipboard!', 'success');
setTimeout(() => updateStatus(''), 2000);
})
.catch(err => {
updateStatus('Failed to copy: ' + err, 'error');
});
}
async function translateText() {
const source = sourceText.value.trim();
if (!source) {
updateStatus('Please enter or speak some text to translate', 'error');
return;
}
updateStatus('Translating...');
translatedText.value = '';
try {
const response = await fetch('/translate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value,
text: source
})
});
const data = await response.json();
if (response.ok) {
translatedText.value = data.translation;
updateStatus('Translation complete', 'success');
setTimeout(() => updateStatus(''), 2000);
} else {
updateStatus(data.error || 'Translation failed', 'error');
}
} catch (error) {
updateStatus('Network error: ' + error.message, 'error');
}
}
function toggleRecording() {
if (!recognition) {
updateStatus('Speech recognition not supported in this browser', 'error');
return;
}
if (isRecording) {
stopRecording();
} else {
startRecording();
}
}
function startRecording() {
sourceText.value = '';
updateStatus('Listening...');
recognition.lang = getLanguageCode(sourceLanguage.value);
recognition.onresult = function(event) {
const transcript = event.results[0][0].transcript;
sourceText.value = transcript;
updateStatus('Recording completed', 'success');
setTimeout(() => updateStatus(''), 2000);
};
recognition.onerror = function(event) {
updateStatus('Error in speech recognition: ' + event.error, 'error');
stopRecording();
};
recognition.onend = function() {
stopRecording();
};
try {
recognition.start();
isRecording = true;
recordSourceButton.classList.add('recording');
recordSourceButton.querySelector('.button-text').textContent = 'Stop';
} catch (error) {
updateStatus('Failed to start recording: ' + error.message, 'error');
}
}
function stopRecording() {
if (isRecording) {
try {
recognition.stop();
} catch (error) {
console.error('Error stopping recognition:', error);
}
isRecording = false;
recordSourceButton.classList.remove('recording');
recordSourceButton.querySelector('.button-text').textContent = 'Record';
}
}
function speakTranslation() {
const text = translatedText.value.trim();
if (!text) {
updateStatus('No translation to speak', 'error');
return;
}
// Use the browser's speech synthesis API
const speech = new SpeechSynthesisUtterance(text);
speech.lang = getLanguageCode(targetLanguage.value);
speech.volume = 1;
speech.rate = 1;
speech.pitch = 1;
speech.onstart = function() {
updateStatus('Speaking...');
speakButton.disabled = true;
};
speech.onend = function() {
updateStatus('');
speakButton.disabled = false;
};
speech.onerror = function(event) {
updateStatus('Speech synthesis error: ' + event.error, 'error');
speakButton.disabled = false;
};
window.speechSynthesis.speak(speech);
}
function getLanguageCode(language) {
// Map language names to BCP 47 language tags for speech recognition/synthesis
const languageMap = {
"arabic": "ar-SA",
"armenian": "hy-AM",
"azerbaijani": "az-AZ",
"english": "en-US",
"french": "fr-FR",
"georgian": "ka-GE",
"kazakh": "kk-KZ",
"mandarin": "zh-CN",
"persian": "fa-IR",
"portuguese": "pt-PT",
"russian": "ru-RU",
"turkish": "tr-TR",
"uzbek": "uz-UZ"
};
return languageMap[language] || 'en-US';
}
function updateStatus(message, type = '') {
statusMessage.textContent = message;
statusMessage.className = 'status-message';
if (type) {
statusMessage.classList.add(type);
}
}
// Check for microphone and speech support when page loads
function checkSupportedFeatures() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
updateStatus('Microphone access is not supported in this browser', 'error');
recordSourceButton.disabled = true;
}
if (!window.SpeechRecognition && !window.webkitSpeechRecognition) {
updateStatus('Speech recognition is not supported in this browser', 'error');
recordSourceButton.disabled = true;
}
if (!window.speechSynthesis) {
updateStatus('Speech synthesis is not supported in this browser', 'error');
speakButton.disabled = true;
}
}
checkSupportedFeatures();
});