V<template>
<div id="app">
<div class="container">
<h1>Real-Time Speech Translation</h1>
<div class="controls">
<button @click="startListening" :disabled="isListening">
{{ isListening ? 'Listening...' : 'Start Listening' }}
</button>
<button @click="stopListening" :disabled="!isListening">
Stop Listening
</button>
<div class="file-upload">
<input type="file" id="audioFile" ref="fileInput" accept=".mp3,.wav,.ogg" @change="handleFileUpload">
<label for="audioFile">Upload Audio File</label>
</div>
</div>
<div class="status">
<p v-if="statusMessage" :class="statusClass">{{ statusMessage }}</p>
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
</div>
<div class="transcription-container">
<div class="transcription-box">
<h2>Original (English)</h2>
<div class="transcription-content">
<p v-for="(line, index) in originalText" :key="'original-'+index">{{ line }}</p>
<p v-if="isProcessing" class="processing-indicator">Processing...</p>
</div>
</div>
<div class="transcription-box">
<h2>Translation (Spanish)</h2>
<div class="transcription-content">
<p v-for="(line, index) in translatedText" :key="'translated-'+index">{{ line }}</p>
<p v-if="isProcessing" class="processing-indicator">Processing...</p>
</div>
</div>
</div>
<div class="metrics-panel" v-if="showMetrics">
<h3>Performance Metrics</h3>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-title">Transcription Time</div>
<div class="metric-value">{{ latestMetrics.transcriptionTime }}s</div>
<div class="metric-subtitle">Speech-to-Text</div>
</div>
<div class="metric-card">
<div class="metric-title">Translation Time</div>
<div class="metric-value">{{ latestMetrics.translationTime }}s</div>
<div class="metric-subtitle">Text-to-Text</div>
</div>
<div class="metric-card">
<div class="metric-title">Total Processing</div>
<div class="metric-value">{{ latestMetrics.totalProcessingTime }}s</div>
<div class="metric-subtitle">End-to-End</div>
</div>
<div class="metric-card">
<div class="metric-title">Average Total</div>
<div class="metric-value">{{ averageMetrics.totalProcessingTime }}s</div>
<div class="metric-subtitle">Running Average</div>
</div>
</div>
<button @click="toggleMetrics" class="toggle-button">Hide Metrics</button>
</div>
<button v-else @click="toggleMetrics" class="toggle-button">Show Metrics</button>
<div class="debug" v-if="debugMode">
<h3>Debug Information</h3>
<pre>Polling Status: {{ pollingActive ? 'Active' : 'Inactive' }}</pre>
<pre>Last Poll: {{ lastPollTime }}</pre>
<pre>Total Transcriptions: {{ totalTranscriptions }}</pre>
<pre>Metrics History: {{ metricsHistory.length }} entries</pre>
<button @click="toggleDebug" class="toggle-button">Hide Debug</button>
</div>
<button v-else @click="toggleDebug" class="toggle-button">Show Debug</button>
</div>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
isListening: false,
isProcessing: false,
pollingActive: false,
pollingInterval: null,
originalText: [],
translatedText: [],
statusMessage: '',
errorMessage: '',
debugMode: true,
showMetrics: true,
lastPollTime: 'Never',
lastSeenIndex: 0,
totalTranscriptions: 0,
baseUrl: 'http://localhost:5001',
metricsHistory: [],
latestMetrics: {
transcriptionTime: 0,
translationTime: 0,
totalProcessingTime: 0
},
averageMetrics: {
transcriptionTime: 0,
translationTime: 0,
totalProcessingTime: 0
}
}
},
computed: {
statusClass() {
return {
'status-info': !this.errorMessage,
'status-error': this.errorMessage
}
}
},
created() {
// Nothing to setup initially
},
beforeDestroy() {
this.stopPolling();
},
methods: {
startPolling() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
this.pollingActive = true;
this.pollingInterval = setInterval(() => {
this.pollTranscriptions();
}, 1000); // Poll every second
},
stopPolling() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
this.pollingInterval = null;
}
this.pollingActive = false;
},
async pollTranscriptions() {
try {
const response = await fetch(`${this.baseUrl}/api/poll_transcriptions?last_seen=${this.lastSeenIndex}`);
if (!response.ok) {
throw new Error('Failed to poll transcriptions');
}
const data = await response.json();
this.lastPollTime = new Date().toLocaleTimeString();
this.totalTranscriptions = data.total_count;
// Process new transcriptions
for (const item of data.transcriptions) {
if (item.error) {
this.handleError(item.error);
} else {
this.handleTranscriptionUpdate(item);
// Process metrics if available
if (item.metrics) {
this.processMetrics(item.metrics);
}
}
}
// Update last seen index
this.lastSeenIndex = data.total_count;
} catch (error) {
console.error('Polling error:', error);
}
},
processMetrics(metrics) {
// Store the latest metrics
this.latestMetrics = {
transcriptionTime: metrics.transcription_time,
translationTime: metrics.translation_time,
totalProcessingTime: metrics.total_processing_time
};
// Add to history
this.metricsHistory.push(this.latestMetrics);
// Calculate running averages
if (this.metricsHistory.length > 0) {
const totals = this.metricsHistory.reduce((acc, curr) => {
return {
transcriptionTime: acc.transcriptionTime + curr.transcriptionTime,
translationTime: acc.translationTime + curr.translationTime,
totalProcessingTime: acc.totalProcessingTime + curr.totalProcessingTime
};
}, { transcriptionTime: 0, translationTime: 0, totalProcessingTime: 0 });
const count = this.metricsHistory.length;
this.averageMetrics = {
transcriptionTime: (totals.transcriptionTime / count).toFixed(3),
translationTime: (totals.translationTime / count).toFixed(3),
totalProcessingTime: (totals.totalProcessingTime / count).toFixed(3)
};
}
// Limit history size to prevent memory issues
if (this.metricsHistory.length > 50) {
this.metricsHistory = this.metricsHistory.slice(-50);
}
},
handleTranscriptionUpdate(payload) {
this.isProcessing = false;
this.originalText.push(payload.original);
this.translatedText.push(payload.translation);
// Auto-scroll to bottom
this.$nextTick(() => {
const containers = document.querySelectorAll('.transcription-content');
containers.forEach(container => {
container.scrollTop = container.scrollHeight;
});
});
},
handleError(error) {
console.error('Error:', error);
this.errorMessage = error;
this.isProcessing = false;
// Clear error after 5 seconds
setTimeout(() => {
this.errorMessage = '';
}, 5000);
},
async startListening() {
this.isListening = true;
this.isProcessing = true;
this.statusMessage = 'Starting microphone listening...';
this.lastSeenIndex = 0; // Reset the counter
this.metricsHistory = []; // Reset metrics history
try {
const response = await fetch(`${this.baseUrl}/api/start_listening`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to start listening');
}
// Start polling for updates
this.startPolling();
this.statusMessage = 'Listening to microphone...';
} catch (error) {
this.handleError(error.message);
this.isListening = false;
this.isProcessing = false;
}
},
async stopListening() {
this.isListening = false;
this.statusMessage = 'Stopping microphone listening...';
try {
const response = await fetch(`${this.baseUrl}/api/stop_listening`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) throw new Error('Failed to stop listening');
// Continue polling for any remaining transcriptions
setTimeout(() => {
this.stopPolling();
}, 3000); // Poll for 3 more seconds to catch final transcriptions
this.statusMessage = 'Microphone stopped';
} catch (error) {
this.handleError(error.message);
}
},
async handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
this.isProcessing = true;
this.statusMessage = `Processing file: ${file.name}`;
this.lastSeenIndex = 0; // Reset the counter
this.metricsHistory = []; // Reset metrics history
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch(`${this.baseUrl}/api/upload`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to upload file');
}
const data = await response.json();
// Start polling for file processing updates
this.startPolling();
this.statusMessage = data.status === 'processing'
? `Processing file: ${file.name}`
: 'File uploaded successfully';
} catch (error) {
this.handleError(error.message);
} finally {
// Reset file input
this.$refs.fileInput.value = '';
}
},
toggleMetrics() {
this.showMetrics = !this.showMetrics;
},
toggleDebug() {
this.debugMode = !this.debugMode;
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin: 0;
padding: 20px;
min-height: 100vh;
background-color: #f5f7fa;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 30px;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
justify-content: center;
}
button {
padding: 10px 15px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
button:hover {
background-color: #3aa876;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.file-upload {
position: relative;
overflow: hidden;
display: inline-block;
}
.file-upload input[type="file"] {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-upload label {
display: inline-block;
padding: 10px 15px;
background-color: #4285f4;
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.file-upload label:hover {
background-color: #3367d6;
}
.status {
margin: 15px 0;
text-align: center;
}
.status-info {
color: #42b983;
}
.status-error {
color: #ff5252;
}
.error {
color: #ff5252;
text-align: center;
}
.transcription-container {
display: flex;
gap: 20px;
margin-top: 20px;
}
.transcription-box {
flex: 1;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
background-color: #f9f9f9;
}
.transcription-box h2 {
margin-top: 0;
color: #2c3e50;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 10px;
}
.transcription-content {
height: 300px;
overflow-y: auto;
padding: 10px;
background-color: white;
border-radius: 4px;
}
.transcription-content p {
margin: 5px 0;
line-height: 1.5;
}
.processing-indicator {
color: #888;
font-style: italic;
}
.metrics-panel {
margin-top: 30px;
padding: 15px;
background-color: #f9f9f9;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.metrics-panel h3 {
margin-top: 0;
color: #2c3e50;
text-align: center;
margin-bottom: 15px;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.metric-card {
background-color: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
text-align: center;
}
.metric-title {
font-weight: bold;
color: #4285f4;
margin-bottom: 5px;
}
.metric-value {
font-size: 1.8em;
font-weight: bold;
color: #2c3e50;
margin: 5px 0;
}
.metric-subtitle {
font-size: 0.9em;
color: #888;
}
.toggle-button {
display: block;
margin: 0 auto;
background-color: #757575;
}
.toggle-button:hover {
background-color: #616161;
}
.debug {
margin-top: 30px;
padding: 15px;
background-color: #f0f0f0;
border-radius: 8px;
font-family: monospace;
}
.debug h3 {
margin-top: 0;
}
@media (max-width: 768px) {
.transcription-container {
flex-direction: column;
}
.metrics-grid {
grid-template-columns: 1fr;
}
}
</style>
make the ui way better. also add a video component with:Subtitle Overlay & UI Integration:
Eventyay Video Integration: Develop a module that overlays interpreted subtitles onto the live video stream from the eventyay video platform.
Customization: Provide configurable options (e.g., font size, color, position, language selection, and toggle controls) to optimize subtitle display for diverse event settings.