Bushra-KB commited on
Commit
69b8b1a
·
verified ·
1 Parent(s): d4eee98

Upload 8 files

Browse files
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>&copy; 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>