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 +1 -0
- Dockerfile +34 -0
- LICENSE +21 -0
- README.md +61 -1
- app.py +228 -0
- april-english-dev-01110_en.april +3 -0
- requirements.txt +10 -0
- static/css/style.css +177 -0
- static/js/script.js +178 -0
- templates/index.html +70 -0
.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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
[](https://huggingface.co/spaces)
|
| 18 |
+
[](https://python.org)
|
| 19 |
+
[](https://fastapi.tiangolo.com)
|
| 20 |
+
[](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>
|