Housekeeping: Remove unnecessary test and temporary files
- Removed test scripts (test_*.py, tts-debug-script.py) - Removed test output files (tts_test_output.mp3, test-cors.html) - Removed redundant static/js/app.js (using TypeScript dist/ instead) - Removed outdated setup-script.sh - Removed Python cache directory (__pycache__) - Removed Claude IDE local settings (.claude/) - Updated .gitignore with better patterns for: - Test files - Debug scripts - Claude IDE settings - Standalone compiled JS This cleanup reduces repository size and removes temporary/debug files that shouldn't be version controlled. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
bcbac5c8b3
commit
b5f2b53262
12
.gitignore
vendored
12
.gitignore
vendored
@ -67,3 +67,15 @@ vapid_public.pem
|
||||
.master_key
|
||||
secrets.db
|
||||
*.key
|
||||
|
||||
# Test files
|
||||
test_*.py
|
||||
*_test_output.*
|
||||
test-*.html
|
||||
*-debug-script.py
|
||||
|
||||
# Claude IDE
|
||||
.claude/
|
||||
|
||||
# Standalone compiled JS (use dist/ instead)
|
||||
static/js/app.js
|
||||
|
776
setup-script.sh
776
setup-script.sh
@ -1,776 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Create necessary directories
|
||||
mkdir -p templates static/{css,js}
|
||||
|
||||
# Move HTML template to templates directory
|
||||
cat > templates/index.html << 'EOL'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Voice Language Translator</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.record-btn {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
margin: 20px auto;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.record-btn:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.recording {
|
||||
background-color: #dc3545 !important;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.card {
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card-header {
|
||||
border-radius: 15px 15px 0 0 !important;
|
||||
}
|
||||
.language-select {
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
.text-display {
|
||||
min-height: 100px;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.btn-action {
|
||||
border-radius: 10px;
|
||||
padding: 8px 15px;
|
||||
margin: 5px;
|
||||
}
|
||||
.spinner-border {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.status-indicator {
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
color: #6c757d;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="text-center mb-4">Voice Language Translator</h1>
|
||||
<p class="text-center text-muted">Powered by Gemma 3, Whisper & Edge TTS</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">Source</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<select id="sourceLanguage" class="form-select language-select mb-3">
|
||||
{% for language in languages %}
|
||||
<option value="{{ language }}">{{ language }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="text-display" id="sourceText">
|
||||
<p class="text-muted">Your transcribed text will appear here...</p>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<button id="playSource" class="btn btn-outline-primary btn-action" disabled>
|
||||
<i class="fas fa-play"></i> Play
|
||||
</button>
|
||||
<button id="clearSource" class="btn btn-outline-secondary btn-action">
|
||||
<i class="fas fa-trash"></i> Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">Translation</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<select id="targetLanguage" class="form-select language-select mb-3">
|
||||
{% for language in languages %}
|
||||
<option value="{{ language }}">{{ language }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="text-display" id="translatedText">
|
||||
<p class="text-muted">Translation will appear here...</p>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<button id="playTranslation" class="btn btn-outline-success btn-action" disabled>
|
||||
<i class="fas fa-play"></i> Play
|
||||
</button>
|
||||
<button id="clearTranslation" class="btn btn-outline-secondary btn-action">
|
||||
<i class="fas fa-trash"></i> Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button id="recordBtn" class="btn btn-primary record-btn">
|
||||
<i class="fas fa-microphone"></i>
|
||||
</button>
|
||||
<p class="status-indicator" id="statusIndicator">Click to start recording</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<button id="translateBtn" class="btn btn-success" disabled>
|
||||
<i class="fas fa-language"></i> Translate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="progress d-none" id="progressContainer">
|
||||
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<audio id="audioPlayer" style="display: none;"></audio>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// DOM elements
|
||||
const recordBtn = document.getElementById('recordBtn');
|
||||
const translateBtn = document.getElementById('translateBtn');
|
||||
const sourceText = document.getElementById('sourceText');
|
||||
const translatedText = document.getElementById('translatedText');
|
||||
const sourceLanguage = document.getElementById('sourceLanguage');
|
||||
const targetLanguage = document.getElementById('targetLanguage');
|
||||
const playSource = document.getElementById('playSource');
|
||||
const playTranslation = document.getElementById('playTranslation');
|
||||
const clearSource = document.getElementById('clearSource');
|
||||
const clearTranslation = document.getElementById('clearTranslation');
|
||||
const statusIndicator = document.getElementById('statusIndicator');
|
||||
const progressContainer = document.getElementById('progressContainer');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const audioPlayer = document.getElementById('audioPlayer');
|
||||
|
||||
// Set initial values
|
||||
let isRecording = false;
|
||||
let mediaRecorder = null;
|
||||
let audioChunks = [];
|
||||
let currentSourceText = '';
|
||||
let currentTranslationText = '';
|
||||
|
||||
// Make sure target language is different from source
|
||||
if (targetLanguage.options[0].value === sourceLanguage.value) {
|
||||
targetLanguage.selectedIndex = 1;
|
||||
}
|
||||
|
||||
// Event listeners for language selection
|
||||
sourceLanguage.addEventListener('change', function() {
|
||||
if (targetLanguage.value === sourceLanguage.value) {
|
||||
for (let i = 0; i < targetLanguage.options.length; i++) {
|
||||
if (targetLanguage.options[i].value !== sourceLanguage.value) {
|
||||
targetLanguage.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
targetLanguage.addEventListener('change', function() {
|
||||
if (targetLanguage.value === sourceLanguage.value) {
|
||||
for (let i = 0; i < sourceLanguage.options.length; i++) {
|
||||
if (sourceLanguage.options[i].value !== targetLanguage.value) {
|
||||
sourceLanguage.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Record button click event
|
||||
recordBtn.addEventListener('click', function() {
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
});
|
||||
|
||||
// Function to start recording
|
||||
function startRecording() {
|
||||
navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
.then(stream => {
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
audioChunks = [];
|
||||
|
||||
mediaRecorder.addEventListener('dataavailable', event => {
|
||||
audioChunks.push(event.data);
|
||||
});
|
||||
|
||||
mediaRecorder.addEventListener('stop', () => {
|
||||
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
||||
transcribeAudio(audioBlob);
|
||||
});
|
||||
|
||||
mediaRecorder.start();
|
||||
isRecording = true;
|
||||
recordBtn.classList.add('recording');
|
||||
recordBtn.classList.replace('btn-primary', 'btn-danger');
|
||||
recordBtn.innerHTML = '<i class="fas fa-stop"></i>';
|
||||
statusIndicator.textContent = 'Recording... Click to stop';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error accessing microphone:', error);
|
||||
alert('Error accessing microphone. Please make sure you have given permission for microphone access.');
|
||||
});
|
||||
}
|
||||
|
||||
// Function to stop recording
|
||||
function stopRecording() {
|
||||
mediaRecorder.stop();
|
||||
isRecording = false;
|
||||
recordBtn.classList.remove('recording');
|
||||
recordBtn.classList.replace('btn-danger', 'btn-primary');
|
||||
recordBtn.innerHTML = '<i class="fas fa-microphone"></i>';
|
||||
statusIndicator.textContent = 'Processing audio...';
|
||||
|
||||
// Stop all audio tracks
|
||||
mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
// Function to transcribe audio
|
||||
function transcribeAudio(audioBlob) {
|
||||
const formData = new FormData();
|
||||
formData.append('audio', audioBlob);
|
||||
formData.append('source_lang', sourceLanguage.value);
|
||||
|
||||
showProgress();
|
||||
|
||||
fetch('/transcribe', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
hideProgress();
|
||||
|
||||
if (data.success) {
|
||||
currentSourceText = data.text;
|
||||
sourceText.innerHTML = `<p>${data.text}</p>`;
|
||||
playSource.disabled = false;
|
||||
translateBtn.disabled = false;
|
||||
statusIndicator.textContent = 'Transcription complete';
|
||||
} else {
|
||||
sourceText.innerHTML = `<p class="text-danger">Error: ${data.error}</p>`;
|
||||
statusIndicator.textContent = 'Transcription failed';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideProgress();
|
||||
console.error('Transcription error:', error);
|
||||
sourceText.innerHTML = `<p class="text-danger">Failed to transcribe audio. Please try again.</p>`;
|
||||
statusIndicator.textContent = 'Transcription failed';
|
||||
});
|
||||
}
|
||||
|
||||
// Translate button click event
|
||||
translateBtn.addEventListener('click', function() {
|
||||
if (!currentSourceText) {
|
||||
return;
|
||||
}
|
||||
|
||||
statusIndicator.textContent = 'Translating...';
|
||||
showProgress();
|
||||
|
||||
fetch('/translate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: currentSourceText,
|
||||
source_lang: sourceLanguage.value,
|
||||
target_lang: targetLanguage.value
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
hideProgress();
|
||||
|
||||
if (data.success) {
|
||||
currentTranslationText = data.translation;
|
||||
translatedText.innerHTML = `<p>${data.translation}</p>`;
|
||||
playTranslation.disabled = false;
|
||||
statusIndicator.textContent = 'Translation complete';
|
||||
} else {
|
||||
translatedText.innerHTML = `<p class="text-danger">Error: ${data.error}</p>`;
|
||||
statusIndicator.textContent = 'Translation failed';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideProgress();
|
||||
console.error('Translation error:', error);
|
||||
translatedText.innerHTML = `<p class="text-danger">Failed to translate. Please try again.</p>`;
|
||||
statusIndicator.textContent = 'Translation failed';
|
||||
});
|
||||
});
|
||||
|
||||
// Play source text
|
||||
playSource.addEventListener('click', function() {
|
||||
if (!currentSourceText) return;
|
||||
|
||||
playAudio(currentSourceText, sourceLanguage.value);
|
||||
statusIndicator.textContent = 'Playing source audio...';
|
||||
});
|
||||
|
||||
// Play translation
|
||||
playTranslation.addEventListener('click', function() {
|
||||
if (!currentTranslationText) return;
|
||||
|
||||
playAudio(currentTranslationText, targetLanguage.value);
|
||||
statusIndicator.textContent = 'Playing translation audio...';
|
||||
});
|
||||
|
||||
// Function to play audio via TTS
|
||||
function playAudio(text, language) {
|
||||
showProgress();
|
||||
|
||||
fetch('/speak', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: text,
|
||||
language: language
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
hideProgress();
|
||||
|
||||
if (data.success) {
|
||||
audioPlayer.src = data.audio_url;
|
||||
audioPlayer.onended = function() {
|
||||
statusIndicator.textContent = 'Ready';
|
||||
};
|
||||
audioPlayer.play();
|
||||
} else {
|
||||
statusIndicator.textContent = 'TTS failed';
|
||||
alert('Failed to play audio: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideProgress();
|
||||
console.error('TTS error:', error);
|
||||
statusIndicator.textContent = 'TTS failed';
|
||||
});
|
||||
}
|
||||
|
||||
// Clear buttons
|
||||
clearSource.addEventListener('click', function() {
|
||||
sourceText.innerHTML = '<p class="text-muted">Your transcribed text will appear here...</p>';
|
||||
currentSourceText = '';
|
||||
playSource.disabled = true;
|
||||
translateBtn.disabled = true;
|
||||
});
|
||||
|
||||
clearTranslation.addEventListener('click', function() {
|
||||
translatedText.innerHTML = '<p class="text-muted">Translation will appear here...</p>';
|
||||
currentTranslationText = '';
|
||||
playTranslation.disabled = true;
|
||||
});
|
||||
|
||||
// Progress indicator functions
|
||||
function showProgress() {
|
||||
progressContainer.classList.remove('d-none');
|
||||
let progress = 0;
|
||||
const interval = setInterval(() => {
|
||||
progress += 5;
|
||||
if (progress > 90) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
progressBar.style.width = `${progress}%`;
|
||||
}, 100);
|
||||
progressBar.dataset.interval = interval;
|
||||
}
|
||||
|
||||
function hideProgress() {
|
||||
const interval = progressBar.dataset.interval;
|
||||
if (interval) {
|
||||
clearInterval(Number(interval));
|
||||
}
|
||||
progressBar.style.width = '100%';
|
||||
setTimeout(() => {
|
||||
progressContainer.classList.add('d-none');
|
||||
progressBar.style.width = '0%';
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
EOL
|
||||
|
||||
# Create app.py
|
||||
cat > app.py << 'EOL'
|
||||
import os
|
||||
import time
|
||||
import tempfile
|
||||
import requests
|
||||
import json
|
||||
from flask import Flask, render_template, request, jsonify, Response, send_file
|
||||
import whisper
|
||||
import torch
|
||||
import ollama
|
||||
import logging
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['UPLOAD_FOLDER'] = tempfile.mkdtemp()
|
||||
app.config['TTS_SERVER'] = os.environ.get('TTS_SERVER_URL', 'http://localhost:5050/v1/audio/speech')
|
||||
app.config['TTS_API_KEY'] = os.environ.get('TTS_API_KEY', 'your_api_key_here')
|
||||
|
||||
# Add a route to check TTS server status
|
||||
@app.route('/check_tts_server', methods=['GET'])
|
||||
def check_tts_server():
|
||||
try:
|
||||
# Try a simple HTTP request to the TTS server
|
||||
response = requests.get(app.config['TTS_SERVER'].rsplit('/api/generate', 1)[0] + '/status', timeout=5)
|
||||
if response.status_code == 200:
|
||||
return jsonify({
|
||||
'status': 'online',
|
||||
'url': app.config['TTS_SERVER']
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'TTS server returned status code {response.status_code}',
|
||||
'url': app.config['TTS_SERVER']
|
||||
})
|
||||
except requests.exceptions.RequestException as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Cannot connect to TTS server: {str(e)}',
|
||||
'url': app.config['TTS_SERVER']
|
||||
})
|
||||
|
||||
# Initialize logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load Whisper model
|
||||
logger.info("Loading Whisper model...")
|
||||
whisper_model = whisper.load_model("base")
|
||||
logger.info("Whisper model loaded successfully")
|
||||
|
||||
# Supported languages
|
||||
SUPPORTED_LANGUAGES = {
|
||||
"ar": "Arabic",
|
||||
"hy": "Armenian",
|
||||
"az": "Azerbaijani",
|
||||
"en": "English",
|
||||
"fr": "French",
|
||||
"ka": "Georgian",
|
||||
"kk": "Kazakh",
|
||||
"zh": "Mandarin",
|
||||
"fa": "Farsi",
|
||||
"pt": "Portuguese",
|
||||
"ru": "Russian",
|
||||
"es": "Spanish",
|
||||
"tr": "Turkish",
|
||||
"uz": "Uzbek"
|
||||
}
|
||||
|
||||
# Map language names to language codes
|
||||
LANGUAGE_TO_CODE = {v: k for k, v in SUPPORTED_LANGUAGES.items()}
|
||||
|
||||
# Map language names to OpenAI TTS voice options
|
||||
LANGUAGE_TO_VOICE = {
|
||||
"Arabic": "alloy", # Using OpenAI general voices
|
||||
"Armenian": "echo", # as OpenAI doesn't have specific voices
|
||||
"Azerbaijani": "nova", # for all these languages
|
||||
"English": "echo", # We'll use the available voices
|
||||
"French": "alloy", # and rely on the translation being
|
||||
"Georgian": "fable", # in the correct language text
|
||||
"Kazakh": "onyx",
|
||||
"Mandarin": "shimmer",
|
||||
"Farsi": "nova",
|
||||
"Portuguese": "alloy",
|
||||
"Russian": "echo",
|
||||
"Spanish": "nova",
|
||||
"Turkish": "fable",
|
||||
"Uzbek": "onyx"
|
||||
}
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html', languages=sorted(SUPPORTED_LANGUAGES.values()))
|
||||
|
||||
@app.route('/transcribe', methods=['POST'])
|
||||
def transcribe():
|
||||
if 'audio' not in request.files:
|
||||
return jsonify({'error': 'No audio file provided'}), 400
|
||||
|
||||
audio_file = request.files['audio']
|
||||
source_lang = request.form.get('source_lang', '')
|
||||
|
||||
# Save the audio file temporarily
|
||||
temp_path = os.path.join(app.config['UPLOAD_FOLDER'], 'input_audio.wav')
|
||||
audio_file.save(temp_path)
|
||||
|
||||
try:
|
||||
# Use Whisper for transcription
|
||||
result = whisper_model.transcribe(
|
||||
temp_path,
|
||||
language=LANGUAGE_TO_CODE.get(source_lang, None)
|
||||
)
|
||||
transcribed_text = result["text"]
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'text': transcribed_text
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Transcription error: {str(e)}")
|
||||
return jsonify({'error': f'Transcription failed: {str(e)}'}), 500
|
||||
finally:
|
||||
# Clean up the temporary file
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
@app.route('/translate', methods=['POST'])
|
||||
def translate():
|
||||
try:
|
||||
data = request.json
|
||||
text = data.get('text', '')
|
||||
source_lang = data.get('source_lang', '')
|
||||
target_lang = data.get('target_lang', '')
|
||||
|
||||
if not text or not source_lang or not target_lang:
|
||||
return jsonify({'error': 'Missing required parameters'}), 400
|
||||
|
||||
# Create a prompt for Gemma 3 translation
|
||||
prompt = f"""
|
||||
Translate the following text from {source_lang} to {target_lang}:
|
||||
|
||||
"{text}"
|
||||
|
||||
Provide only the translation without any additional text.
|
||||
"""
|
||||
|
||||
# Use Ollama to interact with Gemma 3
|
||||
response = ollama.chat(
|
||||
model="gemma3",
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
translated_text = response['message']['content'].strip()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'translation': translated_text
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Translation error: {str(e)}")
|
||||
return jsonify({'error': f'Translation failed: {str(e)}'}), 500
|
||||
|
||||
@app.route('/speak', methods=['POST'])
|
||||
def speak():
|
||||
try:
|
||||
data = request.json
|
||||
text = data.get('text', '')
|
||||
language = data.get('language', '')
|
||||
|
||||
if not text or not language:
|
||||
return jsonify({'error': 'Missing required parameters'}), 400
|
||||
|
||||
voice = LANGUAGE_TO_VOICE.get(language)
|
||||
if not voice:
|
||||
return jsonify({'error': 'Unsupported language for TTS'}), 400
|
||||
|
||||
# Get TTS server URL from environment or config
|
||||
tts_server_url = app.config['TTS_SERVER']
|
||||
|
||||
try:
|
||||
# Request TTS from the Edge TTS server
|
||||
logger.info(f"Sending TTS request to {tts_server_url}")
|
||||
tts_response = requests.post(
|
||||
tts_server_url,
|
||||
json={
|
||||
'text': text,
|
||||
'voice': voice,
|
||||
'output_format': 'mp3'
|
||||
},
|
||||
timeout=10 # Add timeout
|
||||
)
|
||||
|
||||
logger.info(f"TTS response status: {tts_response.status_code}")
|
||||
|
||||
if tts_response.status_code != 200:
|
||||
error_msg = f'TTS request failed with status {tts_response.status_code}'
|
||||
logger.error(error_msg)
|
||||
|
||||
# Try to get error details from response if possible
|
||||
try:
|
||||
error_details = tts_response.json()
|
||||
logger.error(f"Error details: {error_details}")
|
||||
except:
|
||||
pass
|
||||
|
||||
return jsonify({'error': error_msg}), 500
|
||||
|
||||
# The response contains the audio data directly
|
||||
temp_audio_path = os.path.join(app.config['UPLOAD_FOLDER'], f'output_{int(time.time())}.mp3')
|
||||
with open(temp_audio_path, 'wb') as f:
|
||||
f.write(tts_response.content)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'audio_url': f'/get_audio/{os.path.basename(temp_audio_path)}'
|
||||
})
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = f'Failed to connect to TTS server: {str(e)}'
|
||||
logger.error(error_msg)
|
||||
return jsonify({'error': error_msg}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"TTS error: {str(e)}")
|
||||
return jsonify({'error': f'TTS failed: {str(e)}'}), 500
|
||||
|
||||
@app.route('/get_audio/<filename>')
|
||||
def get_audio(filename):
|
||||
try:
|
||||
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||
return send_file(file_path, mimetype='audio/mpeg')
|
||||
except Exception as e:
|
||||
logger.error(f"Audio retrieval error: {str(e)}")
|
||||
return jsonify({'error': f'Audio retrieval failed: {str(e)}'}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
EOL
|
||||
|
||||
# Create requirements.txt
|
||||
cat > requirements.txt << 'EOL'
|
||||
flask==2.3.2
|
||||
requests==2.31.0
|
||||
openai-whisper==20231117
|
||||
torch==2.1.0
|
||||
ollama==0.1.5
|
||||
EOL
|
||||
|
||||
# Create README.md
|
||||
cat > README.md << 'EOL'
|
||||
# Voice Language Translator
|
||||
|
||||
A mobile-friendly web application that translates spoken language between multiple languages using:
|
||||
- Gemma 3 open-source LLM via Ollama for translation
|
||||
- OpenAI Whisper for speech-to-text
|
||||
- OpenAI Edge TTS for text-to-speech
|
||||
|
||||
## Supported Languages
|
||||
|
||||
- Arabic
|
||||
- Armenian
|
||||
- Azerbaijani
|
||||
- English
|
||||
- French
|
||||
- Georgian
|
||||
- Kazakh
|
||||
- Mandarin
|
||||
- Farsi
|
||||
- Portuguese
|
||||
- Russian
|
||||
- Spanish
|
||||
- Turkish
|
||||
- Uzbek
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
1. Install the required Python packages:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Make sure you have Ollama installed and the Gemma 3 model loaded:
|
||||
```
|
||||
ollama pull gemma3
|
||||
```
|
||||
|
||||
3. Ensure your OpenAI Edge TTS server is running on port 5050.
|
||||
|
||||
4. Run the application:
|
||||
```
|
||||
python app.py
|
||||
```
|
||||
|
||||
5. Open your browser and navigate to:
|
||||
```
|
||||
http://localhost:8000
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Select your source language from the dropdown menu
|
||||
2. Press the microphone button and speak
|
||||
3. Press the button again to stop recording
|
||||
4. Wait for the transcription to complete
|
||||
5. Select your target language
|
||||
6. Press the "Translate" button
|
||||
7. Use the play buttons to hear the original or translated text
|
||||
|
||||
## Technical Details
|
||||
|
||||
- The app uses Flask for the web server
|
||||
- Audio is processed client-side using the MediaRecorder API
|
||||
- Whisper for speech recognition with language hints
|
||||
- Ollama provides access to the Gemma 3 model for translation
|
||||
- OpenAI Edge TTS delivers natural-sounding speech output
|
||||
|
||||
## Mobile Support
|
||||
|
||||
The interface is fully responsive and designed to work well on mobile devices.
|
||||
EOL
|
||||
|
||||
# Make the script executable
|
||||
chmod +x app.py
|
||||
|
||||
echo "Setup complete! Run the app with: python app.py"
|
1385
static/js/app.js
1385
static/js/app.js
File diff suppressed because it is too large
Load Diff
228
test-cors.html
228
test-cors.html
@ -1,228 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CORS Test for Talk2Me</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.test-result {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#results {
|
||||
margin-top: 20px;
|
||||
}
|
||||
pre {
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>CORS Test for Talk2Me API</h1>
|
||||
|
||||
<p>This page tests CORS configuration for the Talk2Me API. Open this file from a different origin (e.g., file:// or a different port) to test cross-origin requests.</p>
|
||||
|
||||
<div>
|
||||
<label for="apiUrl">API Base URL:</label>
|
||||
<input type="text" id="apiUrl" placeholder="http://localhost:5005" value="http://localhost:5005">
|
||||
</div>
|
||||
|
||||
<h2>Tests:</h2>
|
||||
|
||||
<button onclick="testHealthEndpoint()">Test Health Endpoint</button>
|
||||
<button onclick="testPreflightRequest()">Test Preflight Request</button>
|
||||
<button onclick="testTranscribeEndpoint()">Test Transcribe Endpoint (OPTIONS)</button>
|
||||
<button onclick="testWithCredentials()">Test With Credentials</button>
|
||||
|
||||
<div id="results"></div>
|
||||
|
||||
<script>
|
||||
function addResult(test, success, message, details = null) {
|
||||
const resultsDiv = document.getElementById('results');
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = `test-result ${success ? 'success' : 'error'}`;
|
||||
|
||||
let html = `<strong>${test}:</strong> ${message}`;
|
||||
if (details) {
|
||||
html += `<pre>${JSON.stringify(details, null, 2)}</pre>`;
|
||||
}
|
||||
resultDiv.innerHTML = html;
|
||||
resultsDiv.appendChild(resultDiv);
|
||||
}
|
||||
|
||||
function getApiUrl() {
|
||||
return document.getElementById('apiUrl').value.trim();
|
||||
}
|
||||
|
||||
async function testHealthEndpoint() {
|
||||
const apiUrl = getApiUrl();
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/health`, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Origin': window.location.origin
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Check CORS headers
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': response.headers.get('Access-Control-Allow-Origin'),
|
||||
'Access-Control-Allow-Credentials': response.headers.get('Access-Control-Allow-Credentials')
|
||||
};
|
||||
|
||||
addResult('Health Endpoint GET', true, 'Request successful', {
|
||||
status: response.status,
|
||||
data: data,
|
||||
corsHeaders: corsHeaders
|
||||
});
|
||||
} catch (error) {
|
||||
addResult('Health Endpoint GET', false, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function testPreflightRequest() {
|
||||
const apiUrl = getApiUrl();
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/api/push-public-key`, {
|
||||
method: 'OPTIONS',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Origin': window.location.origin,
|
||||
'Access-Control-Request-Method': 'GET',
|
||||
'Access-Control-Request-Headers': 'content-type'
|
||||
}
|
||||
});
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': response.headers.get('Access-Control-Allow-Origin'),
|
||||
'Access-Control-Allow-Methods': response.headers.get('Access-Control-Allow-Methods'),
|
||||
'Access-Control-Allow-Headers': response.headers.get('Access-Control-Allow-Headers'),
|
||||
'Access-Control-Max-Age': response.headers.get('Access-Control-Max-Age')
|
||||
};
|
||||
|
||||
addResult('Preflight Request', response.ok, `Status: ${response.status}`, corsHeaders);
|
||||
} catch (error) {
|
||||
addResult('Preflight Request', false, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function testTranscribeEndpoint() {
|
||||
const apiUrl = getApiUrl();
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/transcribe`, {
|
||||
method: 'OPTIONS',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Origin': window.location.origin,
|
||||
'Access-Control-Request-Method': 'POST',
|
||||
'Access-Control-Request-Headers': 'content-type'
|
||||
}
|
||||
});
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': response.headers.get('Access-Control-Allow-Origin'),
|
||||
'Access-Control-Allow-Methods': response.headers.get('Access-Control-Allow-Methods'),
|
||||
'Access-Control-Allow-Headers': response.headers.get('Access-Control-Allow-Headers'),
|
||||
'Access-Control-Allow-Credentials': response.headers.get('Access-Control-Allow-Credentials')
|
||||
};
|
||||
|
||||
addResult('Transcribe Endpoint OPTIONS', response.ok, `Status: ${response.status}`, corsHeaders);
|
||||
} catch (error) {
|
||||
addResult('Transcribe Endpoint OPTIONS', false, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function testWithCredentials() {
|
||||
const apiUrl = getApiUrl();
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/health`, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Origin': window.location.origin
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
addResult('Request with Credentials', true, 'Request successful', {
|
||||
status: response.status,
|
||||
credentialsIncluded: true,
|
||||
data: data
|
||||
});
|
||||
} catch (error) {
|
||||
addResult('Request with Credentials', false, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear results before running new tests
|
||||
function clearResults() {
|
||||
document.getElementById('results').innerHTML = '';
|
||||
}
|
||||
|
||||
// Add event listeners
|
||||
document.querySelectorAll('button').forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
if (!e.target.textContent.includes('Test')) return;
|
||||
clearResults();
|
||||
});
|
||||
});
|
||||
|
||||
// Show current origin
|
||||
window.addEventListener('load', () => {
|
||||
const info = document.createElement('div');
|
||||
info.style.marginBottom = '20px';
|
||||
info.style.padding = '10px';
|
||||
info.style.backgroundColor = '#e9ecef';
|
||||
info.style.borderRadius = '5px';
|
||||
info.innerHTML = `<strong>Current Origin:</strong> ${window.location.origin}<br>
|
||||
<strong>Protocol:</strong> ${window.location.protocol}<br>
|
||||
<strong>Note:</strong> For effective CORS testing, open this file from a different origin than your API server.`;
|
||||
document.body.insertBefore(info, document.querySelector('h2'));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,168 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for error logging system
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from error_logger import ErrorLogger, log_errors, log_performance, get_logger
|
||||
|
||||
def test_basic_logging():
|
||||
"""Test basic logging functionality"""
|
||||
print("\n=== Testing Basic Logging ===")
|
||||
|
||||
# Get logger
|
||||
logger = get_logger('test')
|
||||
|
||||
# Test different log levels
|
||||
logger.debug("This is a debug message")
|
||||
logger.info("This is an info message")
|
||||
logger.warning("This is a warning message")
|
||||
logger.error("This is an error message")
|
||||
|
||||
print("✓ Basic logging test completed")
|
||||
|
||||
def test_error_logging():
|
||||
"""Test error logging with exceptions"""
|
||||
print("\n=== Testing Error Logging ===")
|
||||
|
||||
@log_errors('test.functions')
|
||||
def failing_function():
|
||||
raise ValueError("This is a test error")
|
||||
|
||||
try:
|
||||
failing_function()
|
||||
except ValueError:
|
||||
print("✓ Error was logged")
|
||||
|
||||
# Check if error log exists
|
||||
if os.path.exists('logs/errors.log'):
|
||||
print("✓ Error log file created")
|
||||
|
||||
# Read last line
|
||||
with open('logs/errors.log', 'r') as f:
|
||||
lines = f.readlines()
|
||||
if lines:
|
||||
try:
|
||||
error_entry = json.loads(lines[-1])
|
||||
print(f"✓ Error logged with level: {error_entry.get('level')}")
|
||||
print(f"✓ Error type: {error_entry.get('exception', {}).get('type')}")
|
||||
except json.JSONDecodeError:
|
||||
print("✗ Error log entry is not valid JSON")
|
||||
else:
|
||||
print("✗ Error log file not created")
|
||||
|
||||
def test_performance_logging():
|
||||
"""Test performance logging"""
|
||||
print("\n=== Testing Performance Logging ===")
|
||||
|
||||
@log_performance('test_operation')
|
||||
def slow_function():
|
||||
time.sleep(0.1) # Simulate slow operation
|
||||
return "result"
|
||||
|
||||
result = slow_function()
|
||||
print(f"✓ Function returned: {result}")
|
||||
|
||||
# Check performance log
|
||||
if os.path.exists('logs/performance.log'):
|
||||
print("✓ Performance log file created")
|
||||
|
||||
# Read last line
|
||||
with open('logs/performance.log', 'r') as f:
|
||||
lines = f.readlines()
|
||||
if lines:
|
||||
try:
|
||||
perf_entry = json.loads(lines[-1])
|
||||
duration = perf_entry.get('extra_fields', {}).get('duration_ms', 0)
|
||||
print(f"✓ Performance logged with duration: {duration}ms")
|
||||
except json.JSONDecodeError:
|
||||
print("✗ Performance log entry is not valid JSON")
|
||||
else:
|
||||
print("✗ Performance log file not created")
|
||||
|
||||
def test_structured_logging():
|
||||
"""Test structured logging format"""
|
||||
print("\n=== Testing Structured Logging ===")
|
||||
|
||||
logger = get_logger('test.structured')
|
||||
|
||||
# Log with extra fields
|
||||
logger.info("Structured log test", extra={
|
||||
'extra_fields': {
|
||||
'user_id': 123,
|
||||
'action': 'test_action',
|
||||
'metadata': {'key': 'value'}
|
||||
}
|
||||
})
|
||||
|
||||
# Check main log
|
||||
if os.path.exists('logs/talk2me.log'):
|
||||
with open('logs/talk2me.log', 'r') as f:
|
||||
lines = f.readlines()
|
||||
if lines:
|
||||
try:
|
||||
# Find our test entry
|
||||
for line in reversed(lines):
|
||||
entry = json.loads(line)
|
||||
if entry.get('message') == 'Structured log test':
|
||||
print("✓ Structured log entry found")
|
||||
print(f"✓ Contains timestamp: {'timestamp' in entry}")
|
||||
print(f"✓ Contains hostname: {'hostname' in entry}")
|
||||
print(f"✓ Contains extra fields: {'user_id' in entry}")
|
||||
break
|
||||
except json.JSONDecodeError:
|
||||
print("✗ Log entry is not valid JSON")
|
||||
|
||||
def test_log_rotation():
|
||||
"""Test log rotation settings"""
|
||||
print("\n=== Testing Log Rotation ===")
|
||||
|
||||
# Check if log files exist and their sizes
|
||||
log_files = {
|
||||
'talk2me.log': 'logs/talk2me.log',
|
||||
'errors.log': 'logs/errors.log',
|
||||
'access.log': 'logs/access.log',
|
||||
'security.log': 'logs/security.log',
|
||||
'performance.log': 'logs/performance.log'
|
||||
}
|
||||
|
||||
for name, path in log_files.items():
|
||||
if os.path.exists(path):
|
||||
size = os.path.getsize(path)
|
||||
print(f"✓ {name}: {size} bytes")
|
||||
else:
|
||||
print(f"- {name}: not created yet")
|
||||
|
||||
def main():
|
||||
"""Run all tests"""
|
||||
print("Error Logging System Tests")
|
||||
print("==========================")
|
||||
|
||||
# Create a test Flask app
|
||||
from flask import Flask
|
||||
app = Flask(__name__)
|
||||
app.config['LOG_LEVEL'] = 'DEBUG'
|
||||
app.config['FLASK_ENV'] = 'testing'
|
||||
|
||||
# Initialize error logger
|
||||
error_logger = ErrorLogger(app)
|
||||
|
||||
# Run tests
|
||||
test_basic_logging()
|
||||
test_error_logging()
|
||||
test_performance_logging()
|
||||
test_structured_logging()
|
||||
test_log_rotation()
|
||||
|
||||
print("\n✅ All tests completed!")
|
||||
print("\nCheck the logs directory for generated log files:")
|
||||
print("- logs/talk2me.log - Main application log")
|
||||
print("- logs/errors.log - Error log with stack traces")
|
||||
print("- logs/performance.log - Performance metrics")
|
||||
print("- logs/access.log - HTTP access log")
|
||||
print("- logs/security.log - Security events")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,264 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unit tests for session management system
|
||||
"""
|
||||
import unittest
|
||||
import tempfile
|
||||
import shutil
|
||||
import time
|
||||
import os
|
||||
from session_manager import SessionManager, UserSession, SessionResource
|
||||
from flask import Flask, g, session
|
||||
|
||||
class TestSessionManager(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.config = {
|
||||
'max_session_duration': 3600,
|
||||
'max_idle_time': 900,
|
||||
'max_resources_per_session': 5, # Small limit for testing
|
||||
'max_bytes_per_session': 1024 * 1024, # 1MB for testing
|
||||
'cleanup_interval': 1, # 1 second for faster testing
|
||||
'session_storage_path': self.temp_dir
|
||||
}
|
||||
self.manager = SessionManager(self.config)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test fixtures"""
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_create_session(self):
|
||||
"""Test session creation"""
|
||||
session = self.manager.create_session(
|
||||
session_id='test-123',
|
||||
user_id='user-1',
|
||||
ip_address='127.0.0.1',
|
||||
user_agent='Test Agent'
|
||||
)
|
||||
|
||||
self.assertEqual(session.session_id, 'test-123')
|
||||
self.assertEqual(session.user_id, 'user-1')
|
||||
self.assertEqual(session.ip_address, '127.0.0.1')
|
||||
self.assertEqual(session.user_agent, 'Test Agent')
|
||||
self.assertEqual(len(session.resources), 0)
|
||||
|
||||
def test_get_session(self):
|
||||
"""Test session retrieval"""
|
||||
self.manager.create_session(session_id='test-456')
|
||||
session = self.manager.get_session('test-456')
|
||||
|
||||
self.assertIsNotNone(session)
|
||||
self.assertEqual(session.session_id, 'test-456')
|
||||
|
||||
# Non-existent session
|
||||
session = self.manager.get_session('non-existent')
|
||||
self.assertIsNone(session)
|
||||
|
||||
def test_add_resource(self):
|
||||
"""Test adding resources to session"""
|
||||
self.manager.create_session(session_id='test-789')
|
||||
|
||||
# Add a resource
|
||||
resource = self.manager.add_resource(
|
||||
session_id='test-789',
|
||||
resource_type='audio_file',
|
||||
resource_id='audio-1',
|
||||
path='/tmp/test.wav',
|
||||
size_bytes=1024,
|
||||
metadata={'format': 'wav'}
|
||||
)
|
||||
|
||||
self.assertIsNotNone(resource)
|
||||
self.assertEqual(resource.resource_id, 'audio-1')
|
||||
self.assertEqual(resource.resource_type, 'audio_file')
|
||||
self.assertEqual(resource.size_bytes, 1024)
|
||||
|
||||
# Check session updated
|
||||
session = self.manager.get_session('test-789')
|
||||
self.assertEqual(len(session.resources), 1)
|
||||
self.assertEqual(session.total_bytes_used, 1024)
|
||||
|
||||
def test_resource_limits(self):
|
||||
"""Test resource limit enforcement"""
|
||||
self.manager.create_session(session_id='test-limits')
|
||||
|
||||
# Add resources up to limit
|
||||
for i in range(5):
|
||||
self.manager.add_resource(
|
||||
session_id='test-limits',
|
||||
resource_type='temp_file',
|
||||
resource_id=f'file-{i}',
|
||||
size_bytes=100
|
||||
)
|
||||
|
||||
session = self.manager.get_session('test-limits')
|
||||
self.assertEqual(len(session.resources), 5)
|
||||
|
||||
# Add one more - should remove oldest
|
||||
self.manager.add_resource(
|
||||
session_id='test-limits',
|
||||
resource_type='temp_file',
|
||||
resource_id='file-new',
|
||||
size_bytes=100
|
||||
)
|
||||
|
||||
session = self.manager.get_session('test-limits')
|
||||
self.assertEqual(len(session.resources), 5) # Still 5
|
||||
self.assertNotIn('file-0', session.resources) # Oldest removed
|
||||
self.assertIn('file-new', session.resources) # New one added
|
||||
|
||||
def test_size_limits(self):
|
||||
"""Test size limit enforcement"""
|
||||
self.manager.create_session(session_id='test-size')
|
||||
|
||||
# Add a large resource
|
||||
self.manager.add_resource(
|
||||
session_id='test-size',
|
||||
resource_type='audio_file',
|
||||
resource_id='large-1',
|
||||
size_bytes=500 * 1024 # 500KB
|
||||
)
|
||||
|
||||
# Add another large resource
|
||||
self.manager.add_resource(
|
||||
session_id='test-size',
|
||||
resource_type='audio_file',
|
||||
resource_id='large-2',
|
||||
size_bytes=600 * 1024 # 600KB - would exceed 1MB limit
|
||||
)
|
||||
|
||||
session = self.manager.get_session('test-size')
|
||||
# First resource should be removed to make space
|
||||
self.assertNotIn('large-1', session.resources)
|
||||
self.assertIn('large-2', session.resources)
|
||||
self.assertLessEqual(session.total_bytes_used, 1024 * 1024)
|
||||
|
||||
def test_remove_resource(self):
|
||||
"""Test resource removal"""
|
||||
self.manager.create_session(session_id='test-remove')
|
||||
self.manager.add_resource(
|
||||
session_id='test-remove',
|
||||
resource_type='temp_file',
|
||||
resource_id='to-remove',
|
||||
size_bytes=1000
|
||||
)
|
||||
|
||||
# Remove resource
|
||||
success = self.manager.remove_resource('test-remove', 'to-remove')
|
||||
self.assertTrue(success)
|
||||
|
||||
# Check it's gone
|
||||
session = self.manager.get_session('test-remove')
|
||||
self.assertEqual(len(session.resources), 0)
|
||||
self.assertEqual(session.total_bytes_used, 0)
|
||||
|
||||
def test_cleanup_session(self):
|
||||
"""Test session cleanup"""
|
||||
# Create session with resources
|
||||
self.manager.create_session(session_id='test-cleanup')
|
||||
|
||||
# Create actual temp file
|
||||
temp_file = os.path.join(self.temp_dir, 'test-file.txt')
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write('test content')
|
||||
|
||||
self.manager.add_resource(
|
||||
session_id='test-cleanup',
|
||||
resource_type='temp_file',
|
||||
path=temp_file,
|
||||
size_bytes=12
|
||||
)
|
||||
|
||||
# Cleanup session
|
||||
success = self.manager.cleanup_session('test-cleanup')
|
||||
self.assertTrue(success)
|
||||
|
||||
# Check session is gone
|
||||
session = self.manager.get_session('test-cleanup')
|
||||
self.assertIsNone(session)
|
||||
|
||||
# Check file is deleted
|
||||
self.assertFalse(os.path.exists(temp_file))
|
||||
|
||||
def test_session_info(self):
|
||||
"""Test session info retrieval"""
|
||||
self.manager.create_session(
|
||||
session_id='test-info',
|
||||
ip_address='192.168.1.1'
|
||||
)
|
||||
|
||||
self.manager.add_resource(
|
||||
session_id='test-info',
|
||||
resource_type='audio_file',
|
||||
size_bytes=2048
|
||||
)
|
||||
|
||||
info = self.manager.get_session_info('test-info')
|
||||
self.assertIsNotNone(info)
|
||||
self.assertEqual(info['session_id'], 'test-info')
|
||||
self.assertEqual(info['ip_address'], '192.168.1.1')
|
||||
self.assertEqual(info['resource_count'], 1)
|
||||
self.assertEqual(info['total_bytes_used'], 2048)
|
||||
|
||||
def test_stats(self):
|
||||
"""Test statistics calculation"""
|
||||
# Create multiple sessions
|
||||
for i in range(3):
|
||||
self.manager.create_session(session_id=f'test-stats-{i}')
|
||||
self.manager.add_resource(
|
||||
session_id=f'test-stats-{i}',
|
||||
resource_type='temp_file',
|
||||
size_bytes=1000
|
||||
)
|
||||
|
||||
stats = self.manager.get_stats()
|
||||
self.assertEqual(stats['active_sessions'], 3)
|
||||
self.assertEqual(stats['active_resources'], 3)
|
||||
self.assertEqual(stats['active_bytes'], 3000)
|
||||
self.assertEqual(stats['total_sessions_created'], 3)
|
||||
|
||||
def test_metrics_export(self):
|
||||
"""Test metrics export"""
|
||||
self.manager.create_session(session_id='test-metrics')
|
||||
metrics = self.manager.export_metrics()
|
||||
|
||||
self.assertIn('sessions', metrics)
|
||||
self.assertIn('resources', metrics)
|
||||
self.assertIn('limits', metrics)
|
||||
self.assertEqual(metrics['sessions']['active'], 1)
|
||||
|
||||
class TestFlaskIntegration(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Set up Flask app for testing"""
|
||||
self.app = Flask(__name__)
|
||||
self.app.config['TESTING'] = True
|
||||
self.app.config['SECRET_KEY'] = 'test-secret'
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.app.config['UPLOAD_FOLDER'] = self.temp_dir
|
||||
|
||||
# Initialize session manager
|
||||
from session_manager import init_app
|
||||
init_app(self.app)
|
||||
|
||||
self.client = self.app.test_client()
|
||||
self.ctx = self.app.test_request_context()
|
||||
self.ctx.push()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up"""
|
||||
self.ctx.pop()
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_before_request_handler(self):
|
||||
"""Test Flask before_request integration"""
|
||||
with self.client:
|
||||
# Make a request
|
||||
response = self.client.get('/')
|
||||
|
||||
# Session should be created
|
||||
with self.client.session_transaction() as sess:
|
||||
self.assertIn('session_id', sess)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,146 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for request size limits
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import io
|
||||
import os
|
||||
|
||||
BASE_URL = "http://localhost:5005"
|
||||
|
||||
def test_json_size_limit():
|
||||
"""Test JSON payload size limit"""
|
||||
print("\n=== Testing JSON Size Limit ===")
|
||||
|
||||
# Create a large JSON payload (over 1MB)
|
||||
large_data = {
|
||||
"text": "x" * (2 * 1024 * 1024), # 2MB of text
|
||||
"source_lang": "English",
|
||||
"target_lang": "Spanish"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{BASE_URL}/translate", json=large_data)
|
||||
print(f"Status: {response.status_code}")
|
||||
if response.status_code == 413:
|
||||
print(f"✓ Correctly rejected large JSON: {response.json()}")
|
||||
else:
|
||||
print(f"✗ Should have rejected large JSON")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
def test_audio_size_limit():
|
||||
"""Test audio file size limit"""
|
||||
print("\n=== Testing Audio Size Limit ===")
|
||||
|
||||
# Create a fake large audio file (over 25MB)
|
||||
large_audio = io.BytesIO(b"x" * (30 * 1024 * 1024)) # 30MB
|
||||
|
||||
files = {
|
||||
'audio': ('large_audio.wav', large_audio, 'audio/wav')
|
||||
}
|
||||
data = {
|
||||
'source_lang': 'English'
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{BASE_URL}/transcribe", files=files, data=data)
|
||||
print(f"Status: {response.status_code}")
|
||||
if response.status_code == 413:
|
||||
print(f"✓ Correctly rejected large audio: {response.json()}")
|
||||
else:
|
||||
print(f"✗ Should have rejected large audio")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
def test_valid_requests():
|
||||
"""Test that valid-sized requests are accepted"""
|
||||
print("\n=== Testing Valid Size Requests ===")
|
||||
|
||||
# Small JSON payload
|
||||
small_data = {
|
||||
"text": "Hello world",
|
||||
"source_lang": "English",
|
||||
"target_lang": "Spanish"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{BASE_URL}/translate", json=small_data)
|
||||
print(f"Small JSON - Status: {response.status_code}")
|
||||
if response.status_code != 413:
|
||||
print("✓ Small JSON accepted")
|
||||
else:
|
||||
print("✗ Small JSON should be accepted")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
# Small audio file
|
||||
small_audio = io.BytesIO(b"RIFF" + b"x" * 1000) # 1KB fake WAV
|
||||
files = {
|
||||
'audio': ('small_audio.wav', small_audio, 'audio/wav')
|
||||
}
|
||||
data = {
|
||||
'source_lang': 'English'
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{BASE_URL}/transcribe", files=files, data=data)
|
||||
print(f"Small audio - Status: {response.status_code}")
|
||||
if response.status_code != 413:
|
||||
print("✓ Small audio accepted")
|
||||
else:
|
||||
print("✗ Small audio should be accepted")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
def test_admin_endpoints():
|
||||
"""Test admin endpoints for size limits"""
|
||||
print("\n=== Testing Admin Endpoints ===")
|
||||
|
||||
headers = {'X-Admin-Token': os.environ.get('ADMIN_TOKEN', 'default-admin-token')}
|
||||
|
||||
# Get current limits
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/admin/size-limits", headers=headers)
|
||||
print(f"Get limits - Status: {response.status_code}")
|
||||
if response.status_code == 200:
|
||||
limits = response.json()
|
||||
print(f"✓ Current limits: {limits['limits_human']}")
|
||||
else:
|
||||
print(f"✗ Failed to get limits: {response.text}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
# Update limits
|
||||
new_limits = {
|
||||
"max_audio_size": "30MB",
|
||||
"max_json_size": 2097152 # 2MB in bytes
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{BASE_URL}/admin/size-limits",
|
||||
json=new_limits, headers=headers)
|
||||
print(f"\nUpdate limits - Status: {response.status_code}")
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print(f"✓ Updated limits: {result['new_limits_human']}")
|
||||
else:
|
||||
print(f"✗ Failed to update limits: {response.text}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Request Size Limit Tests")
|
||||
print("========================")
|
||||
print(f"Testing against: {BASE_URL}")
|
||||
print("\nMake sure the Flask app is running on port 5005")
|
||||
|
||||
input("\nPress Enter to start tests...")
|
||||
|
||||
test_valid_requests()
|
||||
test_json_size_limit()
|
||||
test_audio_size_limit()
|
||||
test_admin_endpoints()
|
||||
|
||||
print("\n✅ All tests completed!")
|
@ -1,78 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
TTS Debug Script - Tests connection to the OpenAI TTS server
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
from argparse import ArgumentParser
|
||||
|
||||
def test_tts_connection(server_url, api_key, text="Hello, this is a test message"):
|
||||
"""Test connection to the TTS server"""
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"input": text,
|
||||
"voice": "echo",
|
||||
"response_format": "mp3",
|
||||
"speed": 1.0
|
||||
}
|
||||
|
||||
print(f"Sending request to: {server_url}")
|
||||
print(f"Headers: {headers}")
|
||||
print(f"Payload: {json.dumps(payload, indent=2)}")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
server_url,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=15
|
||||
)
|
||||
|
||||
print(f"Response status code: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
print("Success! Received audio data")
|
||||
# Save to file
|
||||
output_file = "tts_test_output.mp3"
|
||||
with open(output_file, "wb") as f:
|
||||
f.write(response.content)
|
||||
print(f"Saved audio to {output_file}")
|
||||
return True
|
||||
else:
|
||||
print("Error in response")
|
||||
try:
|
||||
error_data = response.json()
|
||||
print(f"Error details: {json.dumps(error_data, indent=2)}")
|
||||
except:
|
||||
print(f"Raw response: {response.text[:500]}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during request: {str(e)}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(description="Test connection to OpenAI TTS server")
|
||||
parser.add_argument("--url", default="http://localhost:5050/v1/audio/speech", help="TTS server URL")
|
||||
parser.add_argument("--key", default=os.environ.get("TTS_API_KEY", ""), help="API key")
|
||||
parser.add_argument("--text", default="Hello, this is a test message", help="Text to synthesize")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.key:
|
||||
print("Error: API key is required. Use --key argument or set TTS_API_KEY environment variable.")
|
||||
return 1
|
||||
|
||||
success = test_tts_connection(args.url, args.key, args.text)
|
||||
return 0 if success else 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user