koesan commited on
Commit
af09686
·
1 Parent(s): b1a78bb

feat: First public release of the VerbaLive app This commit includes the complete application for real-time speech-to-text and translation, with a Flask backend, a JavaScript frontend, and April ASR integration. Includes a Dockerfile for easy deployment on Hugging Face Spaces.

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ april-english-dev-01110_en.april filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Temel Python imajını kullan
2
+ # Bu, uygulamanın çalışması için gerekli tüm Python ortamını sağlar.
3
+ FROM python:3.10-slim
4
+
5
+ # Çalışma dizinini /app olarak belirle
6
+ # Bu, tüm proje dosyalarımızın içinde olacağı dizindir.
7
+ WORKDIR /app
8
+
9
+ # Gerekli sistem kütüphanelerini yükle
10
+ # Bu kütüphaneler, bazı Python paketleri için gerekli olabilir.
11
+ RUN apt-get update && apt-get install -y --no-install-recommends \
12
+ portaudio19-dev \
13
+ gcc \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # requirements.txt dosyasını kopyala ve Python bağımlılıklarını yükle
17
+ # Bu adım, önbellekleme (caching) sayesinde daha hızlı imaj oluşturur.
18
+ COPY requirements.txt .
19
+ RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ # Uygulama dosyalarını Docker imajına kopyala
22
+ # Bu komut, app.py, index.html, static klasörü ve ASR modelini kopyalar.
23
+ COPY . .
24
+
25
+ # Hugging Face Spaces için varsayılan portu ayarla
26
+ # Hugging Face, port numarasını bu ortam değişkeni ile sağlar.
27
+ ARG PORT=5000
28
+ ENV PORT $PORT
29
+ EXPOSE $PORT
30
+
31
+ # Uygulamayı başlat
32
+ # Gunicorn, Flask uygulamanızı daha güvenilir bir şekilde çalıştırır.
33
+ # '--bind 0.0.0.0:$PORT' ifadesi, uygulamayı her yerden erişilebilir yapar.
34
+ CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Barış Enes KÜMET
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -11,4 +11,64 @@ license: mit
11
  short_description: Real-time English captions powered by offline April-ASR, wit
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  short_description: Real-time English captions powered by offline April-ASR, wit
12
  ---
13
 
14
+
15
+ # 🎤 VerbaLive - Real-Time Speech Recognition & Translation
16
+
17
+ [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces)
18
+ [![Python](https://img.shields.io/badge/python-v3.9+-blue.svg)](https://python.org)
19
+ [![FastAPI](https://img.shields.io/badge/FastAPI-green.svg)](https://fastapi.tiangolo.com)
20
+ [![React](https://img.shields.io/badge/React-18+-blue.svg)](https://reactjs.org)
21
+
22
+ VerbaLive, mikrofondan gelen sesi anlık olarak metne dönüştüren ve farklı dillere çeviren gerçek zamanlı konuşma tanıma ve çeviri sistemidir.
23
+
24
+ ## 🌟 Özellikler
25
+
26
+ - 🎯 **Gerçek Zamanlı Ses Tanıma**: Web Audio API ile anlık ses işleme
27
+ - 🌍 **Çok Dilli Çeviri**: Google Translate ile 12+ dil desteği
28
+ - ⚡ **Anlık Çeviri**: Konuşma sırasında satır satır çeviri
29
+ - 📝 **Toplu Çeviri**: Sessizlik algılaması ile tam cümle çevirisi
30
+ - 🎨 **Modern Web Arayüzü**: React tabanlı responsive tasarım
31
+ - 🐳 **Docker Desteği**: Container ortamında çalışır
32
+ - 🤗 **Hugging Face Uyumlu**: Spaces'de deploy edilebilir
33
+
34
+ ## 🚀 Kullanım
35
+
36
+ 1. **Başlat** butonuna tıklayın
37
+ 2. Mikrofon izni verin
38
+ 3. İngilizce konuşun
39
+ 4. Anlık transkripsiyon ve çeviri sonuçlarını görün
40
+
41
+ ## 🔧 Teknik Detaylar
42
+
43
+ - **Backend**: FastAPI + WebSocket
44
+ - **Frontend**: React 18 + Web Audio API
45
+ - **Çeviri**: Google Translate API
46
+ - **Deployment**: Docker + Hugging Face Spaces
47
+
48
+ ## 📱 Arayüz
49
+
50
+ - **🇺🇸 English Speech**: Anlık ses tanıma sonuçları
51
+ - **⚡ Anlık Çeviri**: Gerçek zamanlı çeviri
52
+ - **📝 Toplu Çeviri**: Tamamlanan cümlelerin detaylı çevirisi
53
+
54
+ ## 🌐 Desteklenen Diller
55
+
56
+ 🇹🇷 Türkçe | 🇫🇷 Français | 🇩🇪 Deutsch | 🇪🇸 Español | 🇮🇹 Italiano | 🇷🇺 Русский | 🇯🇵 日本語 | 🇨🇳 中文 | 🇸🇦 العربية | 🇵🇹 Português | 🇳🇱 Nederlands | 🇰🇷 한국어
57
+
58
+ ## 🏗️ Docker Deployment
59
+
60
+ ```bash
61
+ # Build the image
62
+ docker build -t verbalive .
63
+
64
+ # Run the container
65
+ docker run -p 7860:7860 verbalive
66
+ ```
67
+
68
+ ## 📄 License
69
+
70
+ MIT License - Detaylar için [LICENSE](LICENSE) dosyasını inceleyin.
71
+
72
+ ## 👨‍💻 Geliştirici
73
+
74
+ [GitHub Repository](https://github.com/koesan/VerbaLive)
app.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import threading
5
+ from flask import Flask, render_template, request, jsonify, Response
6
+ import numpy as np
7
+ import requests
8
+ import re
9
+ import queue
10
+ import april_asr as april
11
+ import json
12
+
13
+ # Google çeviri için farklı yaklaşım
14
+ try:
15
+ from googletrans import Translator
16
+ GOOGLE_TRANSLATE_AVAILABLE = True
17
+ except Exception as e:
18
+ print(f"Google Translate loading error: {e}")
19
+ GOOGLE_TRANSLATE_AVAILABLE = False
20
+
21
+ try:
22
+ import deepl
23
+ DEEPL_AVAILABLE = True
24
+ except ImportError:
25
+ DEEPL_AVAILABLE = False
26
+
27
+ # Flask uygulamasını başlat
28
+ app = Flask(__name__)
29
+
30
+ # Global değişkenler ve nesneler
31
+ live_caption_instance = None
32
+ is_running = False
33
+ asr_queue = queue.Queue()
34
+ translator = None # Translator nesnesini global tanımla
35
+
36
+ class SimpleGoogleTranslator:
37
+ def __init__(self):
38
+ self.session = requests.Session()
39
+ self.session.headers.update({
40
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
41
+ })
42
+
43
+ def translate(self, text, target_lang='tr', source_lang='en'):
44
+ try:
45
+ url = "https://translate.googleapis.com/translate_a/single"
46
+ params = {
47
+ 'client': 'gtx',
48
+ 'sl': source_lang,
49
+ 'tl': target_lang,
50
+ 'dt': 't',
51
+ 'q': text
52
+ }
53
+ response = self.session.get(url, params=params, timeout=10)
54
+ if response.status_code == 200:
55
+ result = response.json()
56
+ if result and len(result) > 0 and len(result[0]) > 0:
57
+ translated_text = ''.join([item[0] for item in result[0] if item[0]])
58
+ return translated_text
59
+ return None
60
+ except Exception as e:
61
+ print(f"Simple Google Translate error: {e}")
62
+ return None
63
+
64
+ class ImprovedTranslator:
65
+ def __init__(self):
66
+ self.google_translator = None
67
+ self.simple_translator = SimpleGoogleTranslator()
68
+ self.deepl_translator = None
69
+
70
+ def translate(self, text, target_lang, service="Google Translate", deepl_key=""):
71
+ if not text or not text.strip():
72
+ return ""
73
+
74
+ try:
75
+ cleaned_text = self._clean_text(text)
76
+
77
+ if service == "Google Translate":
78
+ return self._google_translate(cleaned_text, target_lang)
79
+ elif service == "DeepL" and DEEPL_AVAILABLE and deepl_key:
80
+ return self._deepl_translate(cleaned_text, target_lang, deepl_key)
81
+ else:
82
+ return f"[{service} not available]"
83
+
84
+ except Exception as e:
85
+ return f"[Translation error: {str(e)[:50]}...]"
86
+
87
+ def _clean_text(self, text):
88
+ text = text.strip()
89
+ text = re.sub(r'\s+', ' ', text)
90
+ return text
91
+
92
+ def _google_translate(self, text, target_lang):
93
+ try:
94
+ result = self.simple_translator.translate(text, target_lang)
95
+ if result:
96
+ return result
97
+ except Exception as e:
98
+ print(f"Simple translator failed: {e}")
99
+
100
+ if GOOGLE_TRANSLATE_AVAILABLE:
101
+ try:
102
+ if self.google_translator is None:
103
+ self.google_translator = Translator()
104
+ result = self.google_translator.translate(text, dest=target_lang, src='en')
105
+ if result and result.text:
106
+ return result.text
107
+ except Exception as e:
108
+ print(f"Googletrans failed: {e}")
109
+
110
+ return "[Google translation failed]"
111
+
112
+ def _deepl_translate(self, text, target_lang, deepl_key):
113
+ try:
114
+ if self.deepl_translator is None:
115
+ self.deepl_translator = deepl.Translator(deepl_key)
116
+
117
+ deepl_lang_map = {
118
+ 'tr': 'TR', 'fr': 'FR', 'de': 'DE', 'es': 'ES',
119
+ 'it': 'IT', 'ru': 'RU', 'ja': 'JA', 'zh-CN': 'ZH',
120
+ 'pt': 'PT', 'nl': 'NL', 'ko': 'KO'
121
+ }
122
+ deepl_target = deepl_lang_map.get(target_lang, target_lang.upper())
123
+
124
+ result = self.deepl_translator.translate_text(text, target_lang=deepl_target)
125
+ return result.text if result else "[DeepL no result]"
126
+
127
+ except Exception as e:
128
+ return f"[DeepL error]"
129
+
130
+ def init_asr():
131
+ """ASR modelini başlatır."""
132
+ global live_caption_instance
133
+ if live_caption_instance is None:
134
+ try:
135
+ model_path = "april-english-dev-01110_en.april"
136
+ if not os.path.exists(model_path):
137
+ print(f"[HATA] Model dosyası bulunamadı: {model_path}")
138
+ return
139
+
140
+ model = april.Model(model_path)
141
+
142
+ def asr_handler(result_type, tokens):
143
+ text = "".join(token.token for token in tokens).strip()
144
+ if not text:
145
+ return
146
+ if result_type == april.Result.PARTIAL_RECOGNITION:
147
+ asr_queue.put({"english": text, "type": "partial"})
148
+ elif result_type == april.Result.FINAL_RECOGNITION:
149
+ asr_queue.put({"english": text, "type": "final"})
150
+
151
+ live_caption_instance = april.Session(model, asr_handler, asynchronous=True)
152
+ print("ASR Model başarıyla başlatıldı.")
153
+ except Exception as e:
154
+ print(f"[HATA] ASR Model yükleme hatası: {e}")
155
+ live_caption_instance = None # Hata durumunda nesneyi None yap
156
+
157
+ @app.route('/')
158
+ def index():
159
+ return render_template('index.html')
160
+
161
+ @app.route('/start', methods=['POST'])
162
+ def start_stream():
163
+ global is_running, translator
164
+ if not is_running:
165
+ init_asr()
166
+ translator = ImprovedTranslator()
167
+ is_running = True
168
+ return jsonify({"status": "started"})
169
+
170
+ @app.route('/stop', methods=['POST'])
171
+ def stop_stream():
172
+ global is_running, live_caption_instance
173
+ if is_running:
174
+ is_running = False
175
+ if live_caption_instance:
176
+ live_caption_instance = None
177
+ return jsonify({"status": "stopped"})
178
+
179
+ @app.route('/upload_audio', methods=['POST'])
180
+ def upload_audio():
181
+ audio_data = request.data
182
+
183
+ if live_caption_instance:
184
+ try:
185
+ live_caption_instance.feed_pcm16(audio_data)
186
+ except Exception as e:
187
+ print(f"[HATA] ASR feed hatası: {e}")
188
+ return jsonify({"status": "error", "message": str(e)}), 500
189
+
190
+ return '', 204
191
+
192
+ @app.route('/stream_results')
193
+ def stream_results():
194
+ target_lang = request.args.get("target_lang", "tr")
195
+ service = request.args.get("service", "Google Translate")
196
+ deepl_key = request.args.get("deepl_key", "")
197
+
198
+ def generate():
199
+ while is_running:
200
+ try:
201
+ result = asr_queue.get(timeout=0.1)
202
+ english_text = result["english"]
203
+ result_type = result["type"]
204
+
205
+ translated_text = ""
206
+ if result_type == "partial":
207
+ translated_text = translator.translate(english_text, target_lang, "Google Translate", "")
208
+ elif result_type == "final":
209
+ translated_text = translator.translate(english_text, target_lang, service, deepl_key)
210
+
211
+ response_data = {
212
+ "english": english_text,
213
+ "type": result_type,
214
+ "translation": translated_text,
215
+ }
216
+ yield f"data: {json.dumps(response_data)}\n\n"
217
+
218
+ except queue.Empty:
219
+ continue
220
+ except Exception as e:
221
+ print(f"SSE hatası: {e}")
222
+ break
223
+
224
+ return Response(generate(), mimetype='text/event-stream')
225
+
226
+ if __name__ == '__main__':
227
+ init_asr()
228
+ app.run(host='0.0.0.0', port=5000, threaded=True)
april-english-dev-01110_en.april ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:77eb95d0fa4f7708a37e868c2265301ee6c42dcb25e63ca63eea3d9cbadbc1f3
3
+ size 336679370
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ april-asr==0.0.4
2
+ sounddevice==0.5.2
3
+ numpy==1.26.4
4
+ PyQt5==5.15.10
5
+ googletrans==3.1.0a0
6
+ deepl==1.22.0
7
+ requests>=2.25.0
8
+ six>=1.15.0
9
+ Flask
10
+ gunicorn
static/css/style.css ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ background-color: #0a0a0a;
3
+ color: #e5e5e7;
4
+ font-family: 'Segoe UI', 'Helvetica Neue', 'Arial', sans-serif;
5
+ font-size: 14px;
6
+ margin: 0;
7
+ padding: 0;
8
+ overflow: hidden; /* Scrollbarları kontrol etmek için */
9
+ }
10
+
11
+ .container {
12
+ display: flex;
13
+ flex-direction: column;
14
+ height: 100vh;
15
+ }
16
+
17
+ /* Kontrol Çubuğu */
18
+ .control-bar {
19
+ display: flex;
20
+ align-items: center;
21
+ background: linear-gradient(to bottom, #1c1c1e, #171719);
22
+ border-bottom: 1px solid #2c2c2e;
23
+ padding: 8px 16px;
24
+ gap: 20px;
25
+ }
26
+
27
+ .title {
28
+ font-size: 17px;
29
+ font-weight: 600;
30
+ color: #ffffff;
31
+ letter-spacing: -0.5px;
32
+ }
33
+
34
+ label {
35
+ font-size: 13px;
36
+ font-weight: 500;
37
+ color: #8e8e93;
38
+ }
39
+
40
+ select, input[type="text"] {
41
+ background: #2c2c2e;
42
+ border: 1px solid #48484a;
43
+ border-radius: 8px;
44
+ padding: 4px 12px;
45
+ color: #ffffff;
46
+ font-size: 13px;
47
+ font-weight: 400;
48
+ min-width: 140px;
49
+ -webkit-appearance: none;
50
+ -moz-appearance: none;
51
+ appearance: none;
52
+ }
53
+ select:hover, input[type="text"]:hover {
54
+ border-color: #0a84ff;
55
+ background: #3a3a3c;
56
+ }
57
+
58
+ input[type="text"] {
59
+ min-width: 280px;
60
+ }
61
+
62
+ #clear-btn {
63
+ background: #ff453a;
64
+ color: white;
65
+ border: none;
66
+ border-radius: 8px;
67
+ padding: 6px 16px;
68
+ font-weight: 500;
69
+ font-size: 13px;
70
+ cursor: pointer;
71
+ margin-left: auto;
72
+ }
73
+ #clear-btn:hover { background: #ff6961; }
74
+ #clear-btn:active { background: #d70015; }
75
+
76
+ /* Ana İçerik Alanı */
77
+ .main-content {
78
+ display: flex;
79
+ flex: 1;
80
+ padding: 6px;
81
+ gap: 6px;
82
+ }
83
+
84
+ .text-group {
85
+ flex: 1;
86
+ border: 1px solid #48484a;
87
+ border-radius: 12px;
88
+ margin-top: 8px;
89
+ padding-top: 16px;
90
+ background: #1c1c1e;
91
+ position: relative;
92
+ display: flex;
93
+ flex-direction: column;
94
+ }
95
+
96
+ .english-group {
97
+ flex: 0 0 500px; /* Genişliği sabit tutmak için */
98
+ }
99
+
100
+ .translation-panel {
101
+ display: flex;
102
+ flex-direction: column;
103
+ flex: 1;
104
+ gap: 6px;
105
+ }
106
+
107
+ .group-title {
108
+ position: absolute;
109
+ top: -8px;
110
+ left: 16px;
111
+ padding: 0 8px;
112
+ background: #1c1c1e;
113
+ color: #ffffff;
114
+ font-size: 13px;
115
+ font-weight: 600;
116
+ letter-spacing: -0.3px;
117
+ }
118
+
119
+ .text-editor {
120
+ background: #000000;
121
+ color: #e5e5e7;
122
+ border: none;
123
+ border-radius: 8px;
124
+ padding: 16px;
125
+ font-size: 16px;
126
+ line-height: 1.6;
127
+ flex: 1;
128
+ overflow-y: auto;
129
+ white-space: pre-wrap;
130
+ word-wrap: break-word;
131
+ }
132
+
133
+ .english-group .text-editor {
134
+ background: #1a0f00;
135
+ color: #ffcc99;
136
+ }
137
+
138
+ /* Durum Çubuğu */
139
+ .status-bar {
140
+ display: flex;
141
+ align-items: center;
142
+ justify-content: space-between;
143
+ background: linear-gradient(to bottom, #1c1c1e, #171719);
144
+ border-top: 1px solid #2c2c2e;
145
+ padding: 4px 16px;
146
+ }
147
+
148
+ #status-label {
149
+ color: #30d158; /* Yeşil */
150
+ font-weight: 500;
151
+ font-size: 12px;
152
+ }
153
+
154
+ .mic-button-container {
155
+ display: flex;
156
+ gap: 10px;
157
+ }
158
+
159
+ #start-mic-btn, #stop-mic-btn {
160
+ background-color: #0a84ff;
161
+ color: white;
162
+ border: none;
163
+ border-radius: 20px;
164
+ padding: 8px 16px;
165
+ font-size: 14px;
166
+ font-weight: 500;
167
+ cursor: pointer;
168
+ }
169
+
170
+ #start-mic-btn:hover { background-color: #38b4ff; }
171
+ #stop-mic-btn { background-color: #ff453a; }
172
+ #stop-mic-btn:hover { background-color: #ff6961; }
173
+
174
+ .version {
175
+ color: #8e8e93;
176
+ font-size: 11px;
177
+ }
static/js/script.js ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const startBtn = document.getElementById('start-mic-btn');
3
+ const stopBtn = document.getElementById('stop-mic-btn');
4
+ const clearBtn = document.getElementById('clear-btn');
5
+ const serviceSelect = document.getElementById('service-select');
6
+ const langSelect = document.getElementById('lang-select');
7
+ const deeplKeyInput = document.getElementById('deepl-key');
8
+ const deeplLabel = document.getElementById('deepl-label');
9
+
10
+ const englishOutput = document.getElementById('english-output');
11
+ const realtimeOutput = document.getElementById('realtime-output');
12
+ const detailedOutput = document.getElementById('detailed-output');
13
+ const statusLabel = document.getElementById('status-label');
14
+
15
+ let isRecording = false;
16
+ let audioContext;
17
+ let processor;
18
+ let eventSource = null;
19
+ let lastEnglishText = ""; // Tamamlanmış son İngilizce metin
20
+ let lastRealtimeText = ""; // Tamamlanmış son çeviri metni
21
+
22
+ // DeepL API key girişini göster/gizle
23
+ serviceSelect.addEventListener('change', (e) => {
24
+ if (e.target.value === 'DeepL') {
25
+ deeplKeyInput.style.display = 'inline-block';
26
+ deeplLabel.style.display = 'inline-block';
27
+ } else {
28
+ deeplKeyInput.style.display = 'none';
29
+ deeplLabel.style.display = 'none';
30
+ }
31
+ });
32
+
33
+ // Ses verisini sunucuya gönderme
34
+ function sendAudioToServer(data) {
35
+ if (!isRecording) return;
36
+ fetch('/upload_audio', {
37
+ method: 'POST',
38
+ body: data,
39
+ headers: {
40
+ 'Content-Type': 'application/octet-stream'
41
+ }
42
+ }).catch(error => console.error('Error sending audio:', error));
43
+ }
44
+
45
+ // SSE bağlantısını başlat
46
+ function startEventSource() {
47
+ const params = new URLSearchParams({
48
+ target_lang: langSelect.value,
49
+ service: serviceSelect.value,
50
+ deepl_key: deeplKeyInput.value
51
+ });
52
+
53
+ eventSource = new EventSource(`/stream_results?${params.toString()}`);
54
+
55
+ eventSource.onmessage = (event) => {
56
+ const data = JSON.parse(event.data);
57
+ const asrText = data.english;
58
+ const asrType = data.type;
59
+ const translation = data.translation;
60
+
61
+ if (asrType === "partial") {
62
+ // Anlık İngilizce ve çeviri metnini güncelle
63
+ englishOutput.textContent = lastEnglishText + asrText;
64
+ realtimeOutput.textContent = lastRealtimeText + translation;
65
+
66
+ } else if (asrType === "final") {
67
+ // Final cümle geldiğinde alt satıra geç ve metinleri sakla
68
+ lastEnglishText += asrText + "\n";
69
+ lastRealtimeText += translation + "\n";
70
+
71
+ // Detaylı çeviriye ekle
72
+ detailedOutput.textContent += asrText + " --> " + translation + "\n";
73
+
74
+ // Anlık çeviri kutusunu sıfırla
75
+ englishOutput.textContent = lastEnglishText;
76
+ realtimeOutput.textContent = lastRealtimeText;
77
+ }
78
+ englishOutput.scrollTop = englishOutput.scrollHeight;
79
+ realtimeOutput.scrollTop = realtimeOutput.scrollHeight;
80
+ detailedOutput.scrollTop = detailedOutput.scrollHeight;
81
+ };
82
+
83
+ eventSource.onerror = (err) => {
84
+ console.error("EventSource hatası:", err);
85
+ statusLabel.textContent = "🔴 Hata: Bağlantı kesildi!";
86
+ statusLabel.style.color = "#ff453a";
87
+ if (eventSource) eventSource.close();
88
+ stopRecording();
89
+ };
90
+ }
91
+
92
+ function stopEventSource() {
93
+ if (eventSource) {
94
+ eventSource.close();
95
+ eventSource = null;
96
+ }
97
+ }
98
+
99
+ // Olay Dinleyicileri
100
+ startBtn.addEventListener('click', async () => {
101
+ if (isRecording) return;
102
+ isRecording = true;
103
+ startBtn.style.display = 'none';
104
+ stopBtn.style.display = 'block';
105
+ statusLabel.textContent = "🟢 Kaydediyor...";
106
+ statusLabel.style.color = "#30d158";
107
+
108
+ // Metin alanlarını temizle
109
+ englishOutput.textContent = "";
110
+ realtimeOutput.textContent = "";
111
+ detailedOutput.textContent = "";
112
+ lastEnglishText = "";
113
+ lastRealtimeText = "";
114
+
115
+ try {
116
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000 } });
117
+ audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
118
+ const source = audioContext.createMediaStreamSource(stream);
119
+
120
+ const bufferSize = 4096;
121
+ processor = audioContext.createScriptProcessor(bufferSize, 1, 1);
122
+
123
+ source.connect(processor);
124
+ processor.connect(audioContext.destination);
125
+
126
+ processor.onaudioprocess = (e) => {
127
+ if (!isRecording) return;
128
+ const pcmData = e.inputBuffer.getChannelData(0);
129
+ const pcm16 = new Int16Array(pcmData.length);
130
+ for (let i = 0; i < pcmData.length; i++) {
131
+ pcm16[i] = Math.max(-1, Math.min(1, pcmData[i])) * 0x7FFF;
132
+ }
133
+ sendAudioToServer(pcm16.buffer);
134
+ };
135
+
136
+ await fetch('/start', { method: 'POST' });
137
+ startEventSource();
138
+
139
+ } catch (err) {
140
+ console.error('Mikrofon erişimi reddedildi veya hata oluştu:', err);
141
+ statusLabel.textContent = "🔴 Hata: Mikrofon erişimi reddedildi!";
142
+ statusLabel.style.color = "#ff453a";
143
+ isRecording = false;
144
+ startBtn.style.display = 'block';
145
+ stopBtn.style.display = 'none';
146
+ }
147
+ });
148
+
149
+ stopBtn.addEventListener('click', () => {
150
+ stopRecording();
151
+ });
152
+
153
+ function stopRecording() {
154
+ if (!isRecording) return;
155
+ isRecording = false;
156
+ if (audioContext) {
157
+ audioContext.close();
158
+ audioContext = null;
159
+ }
160
+
161
+ startBtn.style.display = 'block';
162
+ stopBtn.style.display = 'none';
163
+ statusLabel.textContent = "🔴 Hazır - 'Başlat' butonuna basın";
164
+ statusLabel.style.color = "#30d158";
165
+
166
+ stopEventSource();
167
+ fetch('/stop', { method: 'POST' });
168
+ }
169
+
170
+ clearBtn.addEventListener('click', () => {
171
+ englishOutput.textContent = "";
172
+ realtimeOutput.textContent = "";
173
+ detailedOutput.textContent = "";
174
+ lastEnglishText = "";
175
+ lastRealtimeText = "";
176
+ statusLabel.textContent = "🟢 Tüm veriler temizlendi.";
177
+ });
178
+ });
templates/index.html ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="tr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>VerbaLive - Canlı Konuşma ve Çeviri</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8
+ </head>
9
+ <body>
10
+
11
+ <div class="container">
12
+ <div class="control-bar">
13
+ <span class="title">🎤 VerbaLive</span>
14
+ <span class="separator"></span>
15
+ <label for="service">Servis</label>
16
+ <select id="service-select">
17
+ <option value="Google Translate">Google Translate</option>
18
+ <option value="DeepL">DeepL</option>
19
+ </select>
20
+ <label for="language">Dil</label>
21
+ <select id="lang-select">
22
+ <option value="tr">🇹🇷 Türkçe</option>
23
+ <option value="fr">🇫🇷 Français</option>
24
+ <option value="de">🇩🇪 Deutsch</option>
25
+ <option value="es">🇪🇸 Español</option>
26
+ <option value="it">🇮🇹 Italiano</option>
27
+ <option value="ru">🇷🇺 Русский</option>
28
+ <option value="ja">🇯🇵 日本語</option>
29
+ <option value="zh-CN">🇨🇳 中文</option>
30
+ <option value="ar">🇸🇦 العربية</option>
31
+ <option value="pt">🇵🇹 Português</option>
32
+ <option value="nl">🇳🇱 Nederlands</option>
33
+ <option value="ko">🇰🇷 한국어</option>
34
+ </select>
35
+ <label for="deepl-key" id="deepl-label" style="display:none;">API Key</label>
36
+ <input type="text" id="deepl-key" placeholder="DeepL API anahtarını girin..." style="display:none;">
37
+ <button id="clear-btn">Temizle</button>
38
+ </div>
39
+
40
+ <div class="main-content">
41
+ <div class="text-group english-group">
42
+ <div class="group-title">🇺🇸 İngilizce Konuşma</div>
43
+ <div class="text-editor" id="english-output"></div>
44
+ </div>
45
+
46
+ <div class="translation-panel">
47
+ <div class="text-group realtime-group">
48
+ <div class="group-title">⚡ Anlık Çeviri</div>
49
+ <div class="text-editor" id="realtime-output"></div>
50
+ </div>
51
+ <div class="text-group detailed-group">
52
+ <div class="group-title">📝 Detaylı Çeviri</div>
53
+ <div class="text-editor" id="detailed-output"></div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="status-bar">
59
+ <span id="status-label">🔴 Hazır - 'Başlat' butonuna basın</span>
60
+ <div class="mic-button-container">
61
+ <button id="start-mic-btn">🎤 Başlat</button>
62
+ <button id="stop-mic-btn" style="display:none;">⏹ Durdur</button>
63
+ </div>
64
+ <span class="version">v2.1</span>
65
+ </div>
66
+ </div>
67
+
68
+ <script src="{{ url_for('static', filename='js/script.js') }}"></script>
69
+ </body>
70
+ </html>