Spaces:
Sleeping
Sleeping
Upload 8 files
Browse files- backend/app.py +159 -0
- backend/requirements.txt +8 -0
- backend/static/img/logo.jpg +0 -0
- backend/static/img/logo2.jpg +0 -0
- backend/static/img/logo3.jpg +0 -0
- backend/static/script.js +271 -0
- backend/static/style.css +66 -0
- backend/templates/index.html +242 -0
backend/app.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import io
|
| 3 |
+
import re
|
| 4 |
+
import tempfile
|
| 5 |
+
# Removed heavy imports from top to speed up startup:
|
| 6 |
+
# import torch
|
| 7 |
+
# import numpy as np
|
| 8 |
+
# import soundfile as sf
|
| 9 |
+
from flask import Flask, request, jsonify, send_file, render_template
|
| 10 |
+
from flask_cors import CORS
|
| 11 |
+
from gtts import gTTS
|
| 12 |
+
from gtts.tts import gTTSError
|
| 13 |
+
# Removed top-level transformers import to lazy-load MMS:
|
| 14 |
+
# from transformers import VitsModel, AutoTokenizer
|
| 15 |
+
|
| 16 |
+
# Ensure cache environment variables are set (works locally too)
|
| 17 |
+
os.environ.setdefault("XDG_CACHE_HOME", "/data/.cache")
|
| 18 |
+
os.environ.setdefault("HF_HOME", "/data/.cache/huggingface")
|
| 19 |
+
os.environ.setdefault("HUGGINGFACE_HUB_CACHE", "/data/.cache/huggingface/hub")
|
| 20 |
+
os.environ.setdefault("TRANSFORMERS_CACHE", "/data/transformers")
|
| 21 |
+
os.environ.setdefault("TORCH_HOME", "/data/torch")
|
| 22 |
+
|
| 23 |
+
for p in (
|
| 24 |
+
os.getenv("XDG_CACHE_HOME"),
|
| 25 |
+
os.getenv("HF_HOME"),
|
| 26 |
+
os.getenv("HUGGINGFACE_HUB_CACHE"),
|
| 27 |
+
os.getenv("TRANSFORMERS_CACHE"),
|
| 28 |
+
os.getenv("TORCH_HOME"),
|
| 29 |
+
):
|
| 30 |
+
if p:
|
| 31 |
+
try:
|
| 32 |
+
os.makedirs(p, exist_ok=True)
|
| 33 |
+
except Exception:
|
| 34 |
+
pass
|
| 35 |
+
|
| 36 |
+
# If you load MMS-TTS, pass cache_dir to from_pretrained
|
| 37 |
+
MMS_MODEL_ID = "facebook/mms-tts-amh"
|
| 38 |
+
|
| 39 |
+
_processor = None
|
| 40 |
+
_model = None
|
| 41 |
+
|
| 42 |
+
def load_mms():
|
| 43 |
+
global _processor, _model
|
| 44 |
+
if _processor is not None and _model is not None:
|
| 45 |
+
return _processor, _model
|
| 46 |
+
from transformers import AutoProcessor, AutoModelForTextToSpeech
|
| 47 |
+
cache_dir = os.getenv("TRANSFORMERS_CACHE")
|
| 48 |
+
_processor = AutoProcessor.from_pretrained(MMS_MODEL_ID, cache_dir=cache_dir)
|
| 49 |
+
_model = AutoModelForTextToSpeech.from_pretrained(MMS_MODEL_ID, cache_dir=cache_dir)
|
| 50 |
+
return _processor, _model
|
| 51 |
+
|
| 52 |
+
app = Flask(__name__, static_folder='static', template_folder='templates')
|
| 53 |
+
CORS(app)
|
| 54 |
+
|
| 55 |
+
@app.route('/')
|
| 56 |
+
def index():
|
| 57 |
+
return render_template('index.html')
|
| 58 |
+
|
| 59 |
+
# Health check
|
| 60 |
+
@app.route('/health')
|
| 61 |
+
def health():
|
| 62 |
+
return jsonify({
|
| 63 |
+
"ok": True,
|
| 64 |
+
"mms_loaded": bool(mms_model and mms_tokenizer)
|
| 65 |
+
})
|
| 66 |
+
|
| 67 |
+
@app.route('/api/tts', methods=['POST'])
|
| 68 |
+
def text_to_speech():
|
| 69 |
+
data = request.get_json()
|
| 70 |
+
if not data or 'text' not in data or not data['text'].strip():
|
| 71 |
+
return jsonify({"error": "Text is required."}), 400
|
| 72 |
+
|
| 73 |
+
text = data.get('text')
|
| 74 |
+
model = data.get('model', 'gtts')
|
| 75 |
+
speed = float(data.get('speed', 1.0))
|
| 76 |
+
|
| 77 |
+
print(f"--- Received TTS Request for model: {model} ---")
|
| 78 |
+
|
| 79 |
+
try:
|
| 80 |
+
if model == 'gtts':
|
| 81 |
+
try:
|
| 82 |
+
print("Attempting gTTS synthesis with default endpoint (tld='com')...")
|
| 83 |
+
tts = gTTS(text=text, lang='am', slow=(speed < 1.0), lang_check=False)
|
| 84 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as tmp:
|
| 85 |
+
tmp_path = tmp.name
|
| 86 |
+
try:
|
| 87 |
+
tts.save(tmp_path)
|
| 88 |
+
with open(tmp_path, 'rb') as f:
|
| 89 |
+
data_bytes = f.read()
|
| 90 |
+
finally:
|
| 91 |
+
try: os.remove(tmp_path)
|
| 92 |
+
except OSError: pass
|
| 93 |
+
if not data_bytes:
|
| 94 |
+
raise RuntimeError("gTTS produced empty audio stream")
|
| 95 |
+
audio_fp = io.BytesIO(data_bytes)
|
| 96 |
+
audio_fp.seek(0)
|
| 97 |
+
print("Successfully generated audio with gTTS.")
|
| 98 |
+
return send_file(audio_fp, mimetype='audio/mpeg')
|
| 99 |
+
except gTTSError as ge:
|
| 100 |
+
msg = ("gTTS failed using the default endpoint (Google TTS). "
|
| 101 |
+
"Please try again later or use the MMS model.")
|
| 102 |
+
print(f"gTTS gTTSError: {ge}")
|
| 103 |
+
return jsonify({"error": msg, "details": str(ge)}), 502
|
| 104 |
+
except Exception as ge:
|
| 105 |
+
msg = "gTTS failed unexpectedly on the default endpoint."
|
| 106 |
+
print(f"gTTS unexpected error: {ge}")
|
| 107 |
+
return jsonify({"error": msg, "details": str(ge)}), 502
|
| 108 |
+
|
| 109 |
+
elif model == 'mms':
|
| 110 |
+
try:
|
| 111 |
+
load_mms()
|
| 112 |
+
except Exception as e:
|
| 113 |
+
print(f"Failed to load MMS: {e}")
|
| 114 |
+
return jsonify({"error": "MMS-TTS model is not available on the server.", "details": str(e)}), 500
|
| 115 |
+
|
| 116 |
+
print("Generating audio with MMS-TTS...")
|
| 117 |
+
|
| 118 |
+
# Heavy imports only used here
|
| 119 |
+
import torch
|
| 120 |
+
import soundfile as sf
|
| 121 |
+
|
| 122 |
+
if re.search(r"[^A-Za-z0-9\s\.,\?!;:'\"\-]", text):
|
| 123 |
+
print("Text contains non-Roman characters. Ensure 'uroman' is installed for auto-romanization.")
|
| 124 |
+
|
| 125 |
+
inputs = mms_tokenizer(text, return_tensors="pt")
|
| 126 |
+
try:
|
| 127 |
+
input_len = inputs["input_ids"].shape[-1]
|
| 128 |
+
except Exception:
|
| 129 |
+
input_len = 0
|
| 130 |
+
if input_len == 0:
|
| 131 |
+
msg = ("MMS-TTS received text that tokenized to length 0. "
|
| 132 |
+
"Install 'uroman' (Python >= 3.10) or provide romanized Latin text.")
|
| 133 |
+
print(msg)
|
| 134 |
+
return jsonify({"error": msg}), 400
|
| 135 |
+
|
| 136 |
+
with torch.no_grad():
|
| 137 |
+
output = mms_model(**inputs).waveform
|
| 138 |
+
sampling_rate = mms_model.config.sampling_rate
|
| 139 |
+
speech_waveform = output.cpu().numpy().squeeze()
|
| 140 |
+
|
| 141 |
+
audio_fp = io.BytesIO()
|
| 142 |
+
sf.write(audio_fp, speech_waveform, sampling_rate, format='WAV')
|
| 143 |
+
audio_fp.seek(0)
|
| 144 |
+
print("Successfully generated audio with MMS-TTS.")
|
| 145 |
+
return send_file(audio_fp, mimetype='audio/wav')
|
| 146 |
+
|
| 147 |
+
elif model in ['openai', 'azure']:
|
| 148 |
+
return jsonify({"error": "The keys for this model have expired. Please use other models."}), 403
|
| 149 |
+
|
| 150 |
+
else:
|
| 151 |
+
return jsonify({"error": f"The model '{model}' is not implemented yet."}), 501
|
| 152 |
+
|
| 153 |
+
except Exception as e:
|
| 154 |
+
print(f"An error occurred: {e}")
|
| 155 |
+
return jsonify({"error": f"An unexpected error occurred during TTS generation: {str(e)}"}), 500
|
| 156 |
+
|
| 157 |
+
if __name__ == '__main__':
|
| 158 |
+
port = int(os.getenv('PORT', 7860))
|
| 159 |
+
app.run(debug=False, port=port, host='0.0.0.0')
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask==3.0.0
|
| 2 |
+
Flask-Cors==4.0.0
|
| 3 |
+
gunicorn==21.2.0
|
| 4 |
+
gTTS==2.5.1
|
| 5 |
+
transformers==4.44.2
|
| 6 |
+
soundfile==0.12.1
|
| 7 |
+
numpy==1.26.4
|
| 8 |
+
uroman==1.3.1.1
|
backend/static/img/logo.jpg
ADDED
|
backend/static/img/logo2.jpg
ADDED
|
backend/static/img/logo3.jpg
ADDED
|
backend/static/script.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
$(document).ready(function() {
|
| 2 |
+
// --- DOM Element References ---
|
| 3 |
+
const amharicTextInput = $('#amharicText');
|
| 4 |
+
const ttsModelSelect = $('#ttsModel');
|
| 5 |
+
const speedInput = $('#speed');
|
| 6 |
+
const speedValueDisplay = $('#speed-value');
|
| 7 |
+
const generateBtn = $('#generateBtn');
|
| 8 |
+
const statusMessage = $('#statusMessage');
|
| 9 |
+
const audioPlayer = $('#audioPlayer');
|
| 10 |
+
const downloadBtn = $('#downloadBtn');
|
| 11 |
+
|
| 12 |
+
// --- Other References ---
|
| 13 |
+
const API_URL = '/api/tts';
|
| 14 |
+
let lastGeneratedAudioUrl = null; // Variable to store the blob URL
|
| 15 |
+
|
| 16 |
+
// --- Event Listeners ---
|
| 17 |
+
ttsModelSelect.on('change', updateConfigVisibility);
|
| 18 |
+
|
| 19 |
+
// THIS IS THE CORRECTED LINE
|
| 20 |
+
speedInput.on('input', function() {
|
| 21 |
+
speedValueDisplay.text($(this).val() + 'x');
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
generateBtn.on('click', function() {
|
| 25 |
+
const text = amharicTextInput.val();
|
| 26 |
+
if (!text.trim()) {
|
| 27 |
+
alert('Please enter some Amharic text.');
|
| 28 |
+
return;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const requestData = buildRequestData();
|
| 32 |
+
console.log("Sending data to backend:", JSON.stringify(requestData, null, 2));
|
| 33 |
+
|
| 34 |
+
// --- 1. Set UI to loading state ---
|
| 35 |
+
setLoadingState(true);
|
| 36 |
+
|
| 37 |
+
fetch(API_URL, {
|
| 38 |
+
method: 'POST',
|
| 39 |
+
headers: { 'Content-Type': 'application/json' },
|
| 40 |
+
body: JSON.stringify(requestData),
|
| 41 |
+
})
|
| 42 |
+
.then(handleResponse)
|
| 43 |
+
.then(data => {
|
| 44 |
+
if (data instanceof Blob) {
|
| 45 |
+
console.log("Received audio blob from backend!");
|
| 46 |
+
if (lastGeneratedAudioUrl) {
|
| 47 |
+
URL.revokeObjectURL(lastGeneratedAudioUrl);
|
| 48 |
+
}
|
| 49 |
+
lastGeneratedAudioUrl = URL.createObjectURL(data);
|
| 50 |
+
audioPlayer.attr('src', lastGeneratedAudioUrl);
|
| 51 |
+
setControlsEnabled(true);
|
| 52 |
+
showStatusMessage("Audio generated successfully!", "success");
|
| 53 |
+
}
|
| 54 |
+
})
|
| 55 |
+
.catch(error => {
|
| 56 |
+
console.error('Error during fetch operation:', error);
|
| 57 |
+
showStatusMessage(`Error: ${error.message}`, "danger");
|
| 58 |
+
setControlsEnabled(false);
|
| 59 |
+
})
|
| 60 |
+
.finally(() => {
|
| 61 |
+
setLoadingState(false);
|
| 62 |
+
});
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
downloadBtn.on('click', function() {
|
| 66 |
+
if (!lastGeneratedAudioUrl) return;
|
| 67 |
+
const link = document.createElement('a');
|
| 68 |
+
link.href = lastGeneratedAudioUrl;
|
| 69 |
+
// Choose file extension based on model
|
| 70 |
+
const model = ttsModelSelect.val();
|
| 71 |
+
const ext = (model === 'mms') ? 'wav' : 'mp3';
|
| 72 |
+
link.download = `amharic_speech.${ext}`;
|
| 73 |
+
document.body.appendChild(link);
|
| 74 |
+
link.click();
|
| 75 |
+
document.body.removeChild(link);
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
// --- Helper Functions ---
|
| 79 |
+
function buildRequestData() {
|
| 80 |
+
const model = ttsModelSelect.val();
|
| 81 |
+
let modelSettings = {};
|
| 82 |
+
switch (model) {
|
| 83 |
+
case 'openai': modelSettings.voice = $('#openai-voice').val(); break;
|
| 84 |
+
case 'azure': modelSettings.voice = $('#azure-voice').val(); modelSettings.style = $('#azure-style').val(); break;
|
| 85 |
+
case 'mms': modelSettings.voice = $('#mms-voice').val(); break;
|
| 86 |
+
}
|
| 87 |
+
return {
|
| 88 |
+
text: amharicTextInput.val(),
|
| 89 |
+
model: model,
|
| 90 |
+
speed: speedInput.val(),
|
| 91 |
+
settings: modelSettings
|
| 92 |
+
};
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
function setLoadingState(isLoading) {
|
| 96 |
+
if (isLoading) {
|
| 97 |
+
generateBtn.prop('disabled', true).html('<span class="glyphicon glyphicon-refresh spinning"></span> Generating...');
|
| 98 |
+
statusMessage.html('');
|
| 99 |
+
setControlsEnabled(false);
|
| 100 |
+
} else {
|
| 101 |
+
generateBtn.prop('disabled', false).html('<span class="glyphicon glyphicon-bullhorn"></span> ድምፅ ፍጠር (Generate)');
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
function setControlsEnabled(isEnabled) {
|
| 106 |
+
if (isEnabled) {
|
| 107 |
+
audioPlayer.removeAttr('disabled');
|
| 108 |
+
downloadBtn.removeAttr('disabled');
|
| 109 |
+
} else {
|
| 110 |
+
audioPlayer.attr('disabled', 'disabled');
|
| 111 |
+
downloadBtn.attr('disabled', 'disabled');
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
function showStatusMessage(message, type) {
|
| 116 |
+
statusMessage.html(`<div class="alert alert-${type}">${message}</div>`);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
async function handleResponse(response) {
|
| 120 |
+
if (!response.ok) {
|
| 121 |
+
let errJson = {};
|
| 122 |
+
try { errJson = await response.json(); } catch (e) { /* ignore */ }
|
| 123 |
+
const msg = errJson.details ? `${errJson.error || 'Server returned an error'} - ${errJson.details}` : (errJson.error || 'Server returned an error');
|
| 124 |
+
throw new Error(msg);
|
| 125 |
+
}
|
| 126 |
+
if (response.headers.get("Content-Type").includes("audio")) {
|
| 127 |
+
return response.blob();
|
| 128 |
+
}
|
| 129 |
+
return response.json();
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
function updateConfigVisibility() {
|
| 133 |
+
$('.model-config').hide();
|
| 134 |
+
const selectedModel = ttsModelSelect.val();
|
| 135 |
+
switch (selectedModel) {
|
| 136 |
+
case 'openai': $('#openai-config').show(); $('#speed-config').show(); break;
|
| 137 |
+
case 'azure': $('#azure-config').show(); $('#speed-config').show(); break;
|
| 138 |
+
case 'mms': $('#mms-config').show(); $('#speed-config').show(); break;
|
| 139 |
+
case 'gtts': $('#speed-config').show(); break;
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// --- Initial page setup ---
|
| 144 |
+
const spinningGlyph = `<style>.spinning{animation:spin 1s infinite linear}@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}</style>`;
|
| 145 |
+
$('head').append(spinningGlyph);
|
| 146 |
+
updateConfigVisibility();
|
| 147 |
+
});
|
| 148 |
+
|
| 149 |
+
(function () {
|
| 150 |
+
var $text = $('#amharicText');
|
| 151 |
+
var $char = $('#charCount');
|
| 152 |
+
var $model = $('#ttsModel');
|
| 153 |
+
var $speed = $('#speed');
|
| 154 |
+
var $speedVal = $('#speed-value');
|
| 155 |
+
var $status = $('#statusMessage');
|
| 156 |
+
var $gen = $('#generateBtn');
|
| 157 |
+
var $spinner = $('#genSpinner');
|
| 158 |
+
var $audio = $('#audioPlayer');
|
| 159 |
+
var $download = $('#downloadBtn');
|
| 160 |
+
|
| 161 |
+
function updateCharCount() {
|
| 162 |
+
var n = ($text.val() || '').length;
|
| 163 |
+
$char.text(n + ' characters');
|
| 164 |
+
savePrefs();
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
function updateSpeedValue() {
|
| 168 |
+
$speedVal.text(parseFloat($speed.val()).toFixed(1) + 'x');
|
| 169 |
+
savePrefs();
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
function showModelConfig() {
|
| 173 |
+
var v = $model.val();
|
| 174 |
+
$('.model-config').hide();
|
| 175 |
+
if (v === 'openai') $('#openai-config').show();
|
| 176 |
+
if (v === 'azure') $('#azure-config').show();
|
| 177 |
+
if (v === 'mms') $('#mms-config').show();
|
| 178 |
+
|
| 179 |
+
// Hint about output format
|
| 180 |
+
var hint = (v === 'mms') ? 'Output: WAV for MMS.' : 'Output: MP3 for gTTS/OpenAI/Azure.';
|
| 181 |
+
$('#formatHint').text(hint);
|
| 182 |
+
savePrefs();
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
function loadPrefs() {
|
| 186 |
+
try {
|
| 187 |
+
var p = JSON.parse(localStorage.getItem('ta_prefs') || '{}');
|
| 188 |
+
if (p.text) $text.val(p.text);
|
| 189 |
+
if (p.model) $model.val(p.model);
|
| 190 |
+
if (p.speed) $speed.val(p.speed);
|
| 191 |
+
} catch (e) {}
|
| 192 |
+
}
|
| 193 |
+
function savePrefs() {
|
| 194 |
+
try {
|
| 195 |
+
localStorage.setItem('ta_prefs', JSON.stringify({
|
| 196 |
+
text: $text.val() || '',
|
| 197 |
+
model: $model.val(),
|
| 198 |
+
speed: $speed.val()
|
| 199 |
+
}));
|
| 200 |
+
} catch (e) {}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
function bindButtons() {
|
| 204 |
+
$('#sampleBtn').on('click', function () {
|
| 205 |
+
var sample = 'ሰላም! ይህ ሳምፕል ጽሑፍ ነው። በቀላሉ የአማርኛ ንግግር ድምፅ ለመፍጠር፤ መጀመሪያ የአማርኛ ጽሁፉን እዚህ ይጻፉ ወይ ኮፒ ፔስት ያድርጉት። ቀጥሎ በስተቀኝ ያሉትን ማስተካከያዎች ያስተካክሉ። ከዛም ድምጽ ፍጠር የሚለውን በተን ይጫኑ። ከትንሽ ቆይታዎች በኋላ የተፈጠረውን ድምፅ ማጫወት ወም ማውረድ ይችላሉ። መልካም ግዜ። ';
|
| 206 |
+
$text.val(sample).trigger('input');
|
| 207 |
+
});
|
| 208 |
+
$('#clearBtn').on('click', function () {
|
| 209 |
+
$text.val('').trigger('input');
|
| 210 |
+
$text.focus();
|
| 211 |
+
});
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
// Optional helper: Call window.uiSetLoading(true/false) around your TTS request
|
| 215 |
+
function setLoading(isLoading, msg) {
|
| 216 |
+
if (isLoading) {
|
| 217 |
+
$gen.prop('disabled', true);
|
| 218 |
+
$spinner.show();
|
| 219 |
+
$status
|
| 220 |
+
.removeClass('alert-danger alert-success')
|
| 221 |
+
.addClass('alert alert-info')
|
| 222 |
+
.text(msg || 'Generating audio...');
|
| 223 |
+
} else {
|
| 224 |
+
$gen.prop('disabled', false);
|
| 225 |
+
$spinner.hide();
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
window.uiSetLoading = setLoading;
|
| 229 |
+
|
| 230 |
+
// When audio loads, enable controls and show success
|
| 231 |
+
$audio.on('loadeddata', function () {
|
| 232 |
+
$audio.prop('disabled', false);
|
| 233 |
+
$download.prop('disabled', false);
|
| 234 |
+
$status
|
| 235 |
+
.removeClass('alert-info alert-danger')
|
| 236 |
+
.addClass('alert alert-success')
|
| 237 |
+
.text('Ready. Press play or download.');
|
| 238 |
+
});
|
| 239 |
+
|
| 240 |
+
$(function () {
|
| 241 |
+
$('[data-toggle="tooltip"]').tooltip();
|
| 242 |
+
|
| 243 |
+
loadPrefs();
|
| 244 |
+
updateCharCount();
|
| 245 |
+
updateSpeedValue();
|
| 246 |
+
showModelConfig();
|
| 247 |
+
bindButtons();
|
| 248 |
+
|
| 249 |
+
$text.on('input', updateCharCount);
|
| 250 |
+
$model.on('change', showModelConfig);
|
| 251 |
+
$speed.on('input change', updateSpeedValue);
|
| 252 |
+
});
|
| 253 |
+
})();
|
| 254 |
+
|
| 255 |
+
$(function () {
|
| 256 |
+
// Smooth scroll for in-page links
|
| 257 |
+
$('a[href^="#"]').on('click', function (e) {
|
| 258 |
+
var target = this.getAttribute('href');
|
| 259 |
+
if (target.length > 1 && $(target).length) {
|
| 260 |
+
e.preventDefault();
|
| 261 |
+
$('html, body').animate({ scrollTop: $(target).offset().top - 60 }, 400);
|
| 262 |
+
}
|
| 263 |
+
});
|
| 264 |
+
|
| 265 |
+
// Back to top visibility
|
| 266 |
+
var $top = $('#backToTop');
|
| 267 |
+
$(window).on('scroll', function () {
|
| 268 |
+
if ($(this).scrollTop() > 200) $top.fadeIn();
|
| 269 |
+
else $top.fadeOut();
|
| 270 |
+
});
|
| 271 |
+
});
|
backend/static/style.css
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
background-color: #f4f4f4;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
/* Space for fixed-top navbar */
|
| 6 |
+
body { padding-top: 70px; }
|
| 7 |
+
|
| 8 |
+
/* Brand */
|
| 9 |
+
.navbar-brand { font-size: 22px; font-weight: 700; letter-spacing: 0.3px; }
|
| 10 |
+
@media (min-width: 992px) { .navbar-brand { font-size: 26px; } }
|
| 11 |
+
.navbar-brand .brand-logo {
|
| 12 |
+
height: 28px; width: auto; margin-right: 8px; margin-top: -4px; vertical-align: middle; border-radius: 4px; display: inline-block;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
.navbar-brand .brand-text {
|
| 17 |
+
display: inline-block;
|
| 18 |
+
font-size: 22px;
|
| 19 |
+
font-weight: 700;
|
| 20 |
+
vertical-align: middle;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/* Hero */
|
| 24 |
+
.hero { margin-top: 10px; padding: 30px 20px; }
|
| 25 |
+
.hero-title { margin-top: 10px; margin-bottom: 10px; }
|
| 26 |
+
.hero-subtitle { font-size: 16px; color: #555; }
|
| 27 |
+
.hero-link { text-decoration: underline; }
|
| 28 |
+
|
| 29 |
+
/* Subtle elevation for panels */
|
| 30 |
+
.panel-elevated { box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
|
| 31 |
+
.panel-elevated:hover { box-shadow: 0 3px 10px rgba(0,0,0,0.08); transition: box-shadow .2s ease; }
|
| 32 |
+
|
| 33 |
+
/* Icon spacing */
|
| 34 |
+
.navbar-nav > li > a .glyphicon, .navbar-nav > li > a .fa { margin-right: 6px; }
|
| 35 |
+
.btn .glyphicon, .btn .fa { margin-right: 6px; }
|
| 36 |
+
|
| 37 |
+
/* Contact spacing */
|
| 38 |
+
.contact-section .panel { margin-top: 20px; }
|
| 39 |
+
|
| 40 |
+
/* Footer */
|
| 41 |
+
.footer { margin: 40px 0 20px; padding: 12px 0; border-top: 1px solid #eee; color: #777; }
|
| 42 |
+
|
| 43 |
+
/* Back to top */
|
| 44 |
+
.back-to-top {
|
| 45 |
+
position: fixed; right: 16px; bottom: 20px; z-index: 1030;
|
| 46 |
+
display: none; background: #337ab7; color: #fff; padding: 8px 10px;
|
| 47 |
+
border-radius: 4px; box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
| 48 |
+
}
|
| 49 |
+
.back-to-top:hover { text-decoration: none; background: #2e6da4; }
|
| 50 |
+
|
| 51 |
+
/* Spinner animation */
|
| 52 |
+
.glyphicon-refresh-animate { -webkit-animation: spin 1s infinite linear; animation: spin 1s infinite linear; }
|
| 53 |
+
@-webkit-keyframes spin { from { -webkit-transform: rotate(0deg);} to { -webkit-transform: rotate(359deg);} }
|
| 54 |
+
@keyframes spin { from { transform: rotate(0deg);} to { transform: rotate(359deg);} }
|
| 55 |
+
|
| 56 |
+
/* Dark mode (optional) */
|
| 57 |
+
@media (prefers-color-scheme: dark) {
|
| 58 |
+
body { background: #121212; color: #ddd; }
|
| 59 |
+
.navbar-default { background-color: #1f1f1f; border-color: #2a2a2a; }
|
| 60 |
+
.navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a { color: #ddd; }
|
| 61 |
+
.jumbotron.hero { background-color: #1b1b1b; color: #ddd; }
|
| 62 |
+
.panel { background-color: #1a1a1a; border-color: #2a2a2a; }
|
| 63 |
+
.panel .panel-heading { background-color: #252525 !important; color: #eee; border-color: #2a2a2a; }
|
| 64 |
+
.help-block { color: #aaa; }
|
| 65 |
+
.footer { border-top-color: #2a2a2a; color: #aaa; }
|
| 66 |
+
}
|
backend/templates/index.html
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>NegaritAI - A Web Based Amharic Text to Speech</title>
|
| 7 |
+
<!-- Bootstrap 3 CSS -->
|
| 8 |
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
|
| 9 |
+
<!-- Font Awesome (icons for GitHub, etc.) -->
|
| 10 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
| 11 |
+
<!-- Custom CSS -->
|
| 12 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
| 13 |
+
<!-- Favicon -->
|
| 14 |
+
<link rel="icon" href="{{ url_for('static', filename='img/log3.jpg') }}" type="image/jpeg">
|
| 15 |
+
</head>
|
| 16 |
+
<body>
|
| 17 |
+
|
| 18 |
+
<nav class="navbar navbar-default navbar-fixed-top">
|
| 19 |
+
<div class="container">
|
| 20 |
+
<div class="navbar-header">
|
| 21 |
+
<a class="navbar-brand" href="#">
|
| 22 |
+
<img src="{{ url_for('static', filename='img/logo3.jpg') }}" alt="NegaritAI logo" class="brand-logo">
|
| 23 |
+
<span class="brand-text">NegaritAI</span>
|
| 24 |
+
</a>
|
| 25 |
+
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#top-nav">
|
| 26 |
+
<span class="icon-bar"></span><span class="icon-bar"></span><span class="icon-bar"></span>
|
| 27 |
+
</button>
|
| 28 |
+
</div>
|
| 29 |
+
<div id="top-nav" class="collapse navbar-collapse">
|
| 30 |
+
<ul class="nav navbar-nav navbar-right">
|
| 31 |
+
<li><a href="https://huggingface.co/spaces/bushra-kb/talk-amharic-tts" target="_blank"><i class="fa fa-cloud"></i> Space</a></li>
|
| 32 |
+
<li><a href="https://github.com/bushra-kb/" target="_blank"><i class="fa fa-github"></i> GitHub</a></li>
|
| 33 |
+
<li><a href="https://github.com/bushra-kb/" target="_blank"><span class="glyphicon glyphicon-book"></span> Docs</a></li>
|
| 34 |
+
<li><a href="#contact"><span class="glyphicon glyphicon-earphone"></span> Contact Me</a></li>
|
| 35 |
+
</ul>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
</nav>
|
| 39 |
+
|
| 40 |
+
<div class="container">
|
| 41 |
+
<!-- Hero -->
|
| 42 |
+
<div class="jumbotron hero text-center">
|
| 43 |
+
<p class="hero-subtitle">
|
| 44 |
+
<strong>NegaritAI: </strong>
|
| 45 |
+
Practical AI for Ethiopian languages—this page showcases the Amharic Text‑to‑Speech demo.<br>
|
| 46 |
+
Developed by <a href="https://github.com/bushra-kb/" class="hero-link">Bushra KB</a> (2025).
|
| 47 |
+
</p>
|
| 48 |
+
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<!-- Demo section -->
|
| 52 |
+
<div id="demo" class="row">
|
| 53 |
+
<div class="col-md-8">
|
| 54 |
+
<div class="panel panel-primary panel-elevated">
|
| 55 |
+
<div class="panel-heading">
|
| 56 |
+
<h3 class="panel-title">
|
| 57 |
+
<span class="glyphicon glyphicon-edit" aria-hidden="true"></span>
|
| 58 |
+
የጽሑፍ ማስገቢያ (Text Input)
|
| 59 |
+
</h3>
|
| 60 |
+
</div>
|
| 61 |
+
<div class="panel-body">
|
| 62 |
+
<textarea id="amharicText" class="form-control" rows="6" placeholder="የአማርኛ ጽሑፍ እዚህ ያስገቡ... (Enter Amharic text here...)"></textarea>
|
| 63 |
+
<div class="clearfix" style="margin-top:8px;">
|
| 64 |
+
<div class="pull-left text-muted">
|
| 65 |
+
<small id="charCount">0 characters</small>
|
| 66 |
+
</div>
|
| 67 |
+
<div class="pull-right">
|
| 68 |
+
<div class="btn-group btn-group-sm" role="group" aria-label="Text actions">
|
| 69 |
+
<button id="sampleBtn" type="button" class="btn btn-default" title="Insert sample text" data-toggle="tooltip">
|
| 70 |
+
<span class="glyphicon glyphicon-file"></span> Sample
|
| 71 |
+
</button>
|
| 72 |
+
<button id="clearBtn" type="button" class="btn btn-default" title="Clear text" data-toggle="tooltip">
|
| 73 |
+
<span class="glyphicon glyphicon-trash"></span> Clear
|
| 74 |
+
</button>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
<p class="help-block">Tip: Write or paste any Amharic text. Configure settings, then click “Generate”.</p>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
<div class="col-md-4">
|
| 83 |
+
<div class="panel panel-info panel-elevated">
|
| 84 |
+
<div class="panel-heading">
|
| 85 |
+
<h3 class="panel-title">
|
| 86 |
+
<span class="glyphicon glyphicon-cog" aria-hidden="true"></span>
|
| 87 |
+
ማስተካከያዎች (Configurations)
|
| 88 |
+
</h3>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="panel-body">
|
| 91 |
+
<form class="form-horizontal">
|
| 92 |
+
<!-- TTS Model Selection -->
|
| 93 |
+
<div class="form-group">
|
| 94 |
+
<label for="ttsModel" class="col-sm-3 control-label">TTS ሞዴል (Model)</label>
|
| 95 |
+
<div class="col-sm-9">
|
| 96 |
+
<select id="ttsModel" class="form-control" title="Choose a TTS provider">
|
| 97 |
+
<option value="gtts">gTTS (Google)</option>
|
| 98 |
+
<option value="mms">Hugging Face MMS-TTS</option>
|
| 99 |
+
<option value="openai">OpenAI TTS</option>
|
| 100 |
+
<option value="azure">Microsoft Azure TTS</option>
|
| 101 |
+
</select>
|
| 102 |
+
<p class="help-block" id="formatHint">Output: MP3 for gTTS/OpenAI/Azure, WAV for MMS.</p>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<!-- Common Configurations: Speed -->
|
| 107 |
+
<div class="form-group config-group" id="speed-config">
|
| 108 |
+
<label for="speed" class="col-sm-3 control-label">ፍጥነት (Speed)</label>
|
| 109 |
+
<div class="col-sm-7">
|
| 110 |
+
<input type="range" id="speed" class="form-control" min="0.5" max="2" step="0.1" value="1" aria-label="Playback speed">
|
| 111 |
+
</div>
|
| 112 |
+
<div class="col-sm-2">
|
| 113 |
+
<p class="form-control-static" id="speed-value">1.0x</p>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<!-- OpenAI Specific Configurations -->
|
| 118 |
+
<div class="model-config" id="openai-config">
|
| 119 |
+
<div class="form-group">
|
| 120 |
+
<label for="openai-voice" class="col-sm-3 control-label">ድምፅ (Voice)</label>
|
| 121 |
+
<div class="col-sm-9">
|
| 122 |
+
<select id="openai-voice" class="form-control">
|
| 123 |
+
<option value="alloy">Alloy</option>
|
| 124 |
+
<option value="echo">Echo</option>
|
| 125 |
+
<option value="fable">Fable</option>
|
| 126 |
+
<option value="onyx">Onyx</option>
|
| 127 |
+
<option value="nova">Nova</option>
|
| 128 |
+
<option value="shimmer">Shimmer</option>
|
| 129 |
+
</select>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<!-- Azure Specific Configurations -->
|
| 135 |
+
<div class="model-config" id="azure-config">
|
| 136 |
+
<div class="form-group">
|
| 137 |
+
<label for="azure-voice" class="col-sm-3 control-label">ድምፅ (Voice)</label>
|
| 138 |
+
<div class="col-sm-9">
|
| 139 |
+
<select id="azure-voice" class="form-control">
|
| 140 |
+
<option value="am-ET-Mekdes-Neural">Mekdes (Female)</option>
|
| 141 |
+
<option value="am-ET-Ameha-Neural">Ameha (Male)</option>
|
| 142 |
+
</select>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
<div class="form-group">
|
| 146 |
+
<label for="azure-style" class="col-sm-3 control-label">የንግግር ስልት (Style)</label>
|
| 147 |
+
<div class="col-sm-9">
|
| 148 |
+
<select id="azure-style" class="form-control">
|
| 149 |
+
<option value="general">General</option>
|
| 150 |
+
<option value="cheerful">Cheerful</option>
|
| 151 |
+
<option value="sad">Sad</option>
|
| 152 |
+
</select>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<!-- MMS-TTS Specific Configurations -->
|
| 158 |
+
<div class="model-config" id="mms-config">
|
| 159 |
+
<div class="form-group">
|
| 160 |
+
<label for="mms-voice" class="col-sm-3 control-label">ድምፅ (Voice)</label>
|
| 161 |
+
<div class="col-sm-9">
|
| 162 |
+
<select id="mms-voice" class="form-control">
|
| 163 |
+
<option value="default">Default</option>
|
| 164 |
+
</select>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
</form>
|
| 170 |
+
</div>
|
| 171 |
+
<div class="panel-footer">
|
| 172 |
+
<button id="generateBtn" class="btn btn-block btn-success">
|
| 173 |
+
<span class="glyphicon glyphicon-bullhorn"></span>
|
| 174 |
+
ድምፅ ፍጠር (Generate)
|
| 175 |
+
<span id="genSpinner" class="glyphicon glyphicon-refresh glyphicon-refresh-animate" style="margin-left:8px; display:none;"></span>
|
| 176 |
+
</button>
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
<div id="audio" class="row text-center" >
|
| 183 |
+
<div class="col-md-8 col-md-offset-2">
|
| 184 |
+
<div id="statusMessage" role="status" style="margin-bottom: 15px;" aria-live="polite"></div>
|
| 185 |
+
<div class="panel panel-info panel-elevated">
|
| 186 |
+
<div class="panel-heading">
|
| 187 |
+
<h3 class="panel-title">የተፈጠረው ድምፅ (Generated Audio)</h3>
|
| 188 |
+
</div>
|
| 189 |
+
<div class="panel-body">
|
| 190 |
+
<audio id="audioPlayer" controls disabled style="width: 100%;"></audio>
|
| 191 |
+
<button id="downloadBtn" class="btn btn-primary" style="margin-top: 15px;" disabled>
|
| 192 |
+
<span class="glyphicon glyphicon-download-alt"></span> አውርድ (Download)
|
| 193 |
+
</button>
|
| 194 |
+
<p class="help-block" style="margin-top:10px;">
|
| 195 |
+
First play may take longer after idle time. Heavy usage can be rate-limited.
|
| 196 |
+
</p>
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
<!-- Contact section -->
|
| 203 |
+
<section id="contact" class="contact-section">
|
| 204 |
+
<div class="row">
|
| 205 |
+
<div class="col-md-8 col-md-offset-2">
|
| 206 |
+
<div class="panel panel-default">
|
| 207 |
+
<div class="panel-heading">
|
| 208 |
+
<h3 class="panel-title"><span class="glyphicon glyphicon-envelope"></span> Contact Me</h3>
|
| 209 |
+
</div>
|
| 210 |
+
<div class="panel-body text-center">
|
| 211 |
+
<p>Questions, feedback, or collaboration? Welcome, Bro! Here are some ways to reach out:</p>
|
| 212 |
+
<div class="btn-group">
|
| 213 |
+
<a class="btn btn-default" href="mailto:[email protected]"><i class="fa fa-envelope"></i> Email</a>
|
| 214 |
+
<a class="btn btn-default" href="https://github.com/bushra-kb/talk-amharic-tts/issues" target="_blank"><i class="fa fa-github"></i> GitHub Issues</a>
|
| 215 |
+
<a class="btn btn-default" href="https://huggingface.co/spaces/bushra-kb/talk-amharic-tts/discussions" target="_blank"><span class="glyphicon glyphicon-comment"></span> Discussions</a>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
</section>
|
| 222 |
+
|
| 223 |
+
<!-- Footer -->
|
| 224 |
+
<footer class="footer text-center text-muted">
|
| 225 |
+
<small>© 2025 NegaritAI • Developed by Bushra KB</small>
|
| 226 |
+
</footer>
|
| 227 |
+
</div>
|
| 228 |
+
|
| 229 |
+
<!-- Back to top -->
|
| 230 |
+
<a href="#top" id="backToTop" class="back-to-top" title="Back to top">
|
| 231 |
+
<span class="glyphicon glyphicon-chevron-up"></span>
|
| 232 |
+
</a>
|
| 233 |
+
|
| 234 |
+
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
| 235 |
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
|
| 236 |
+
<!-- Bootstrap 3 JavaScript -->
|
| 237 |
+
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
| 238 |
+
<!-- Custom JS -->
|
| 239 |
+
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
| 240 |
+
|
| 241 |
+
</body>
|
| 242 |
+
</html>
|