MIC-Off / app.py
VethaNarayananG's picture
Force port 7860 on Hugging Face Spaces
8de01c0
Raw
History Blame Contribute Delete
13.9 kB
"""
app.py β€” MIC Translator Flask Backend
Clean routing layer using modular components.
"""
import os
import time
import traceback
from datetime import datetime
from flask import Flask, render_template, request, jsonify, Response
from flask_cors import CORS
os.environ["TOKENIZERS_PARALLELISM"] = "false"
from config import APP_TITLE, APP_VERSION, LANGUAGE_NAMES, NLLB_LANGS
from speech import SpeechRecognizer
from translator import Translator
from language_detector import LanguageDetector
from correction_engine import CorrectionEngine
from dataset_loader import DatasetLoader
from tts import TextToSpeech
from history import TranslationHistory
# ═══════════════════════════════════════════════════════════════════════════════
# FLASK SETUP
# ═══════════════════════════════════════════════════════════════════════════════
app = Flask(
__name__,
template_folder="templates",
static_folder="static"
)
CORS(app)
# ═══════════════════════════════════════════════════════════════════════════════
# INITIALIZE MODULES
# ═══════════════════════════════════════════════════════════════════════════════
speech = SpeechRecognizer()
translator = Translator()
detector = LanguageDetector()
corrector = CorrectionEngine()
datasets = DatasetLoader()
tts = TextToSpeech()
history = TranslationHistory()
def init_modules():
"""Load all modules at startup."""
print(f"\n{'='*50}")
print(f" {APP_TITLE} {APP_VERSION}")
print(f" Initializing modules...")
print(f"{'='*50}\n")
speech.load()
translator.load()
corrector.load()
datasets.load_all()
tts.load()
history.load()
lang_count = len(NLLB_LANGS)
print(f"\n{'='*50}")
print(f" [OK] All modules loaded!")
print(f" Languages: {lang_count} supported")
print(f" Corrections: {corrector.get_stats()['total']} loaded")
print(f" Datasets: {datasets.get_stats()['total_pairs']} pairs loaded")
print(f" Voices: {len(tts.get_available_voices())} voice models")
print(f"{'='*50}\n")
# ═══════════════════════════════════════════════════════════════════════════════
# ROUTES β€” Pages
# ═══════════════════════════════════════════════════════════════════════════════
@app.route("/")
def home():
return render_template("index.html")
# ═══════════════════════════════════════════════════════════════════════════════
# ROUTES β€” API
# ═══════════════════════════════════════════════════════════════════════════════
@app.route("/api/test")
def api_test():
return jsonify({"success": True, "message": "Backend working"})
@app.route("/api/status")
def api_status():
return jsonify({
"whisper_ready": speech.ready,
"translator_ready": translator.ready,
"tts_ready": tts.ready,
"corrections_loaded": corrector.loaded,
"language_count": len(NLLB_LANGS),
"voice_count": len(tts.get_available_voices()),
"dataset_stats": datasets.get_stats(),
"correction_stats": corrector.get_stats(),
"version": APP_VERSION
})
# ── Languages ────────────────────────────────────────────────────────────────
@app.route("/api/languages")
def api_languages():
"""Return all supported languages with metadata."""
languages = {}
for code, name in sorted(LANGUAGE_NAMES.items(), key=lambda x: x[1]):
languages[code] = {
"name": name,
"nllb_code": NLLB_LANGS.get(code, ""),
"has_voice": code in tts.get_available_voices()
}
return jsonify(languages)
@app.route("/api/languages/favorites")
def api_get_favorites():
return jsonify({
"favorites": history.get_favorites(),
"recent": history.get_recent_langs()
})
@app.route("/api/languages/favorites", methods=["POST"])
def api_set_favorite():
data = request.get_json()
action = data.get("action", "add")
lang = data.get("lang_code", "")
if action == "add":
history.add_favorite(lang)
elif action == "remove":
history.remove_favorite(lang)
return jsonify({"success": True, "favorites": history.get_favorites()})
# ── Transcribe (Speech-to-Text) ──────────────────────────────────────────────
@app.route("/api/transcribe", methods=["POST"])
def api_transcribe():
"""Transcribe audio to text using Whisper."""
if "audio" not in request.files:
return jsonify({"error": "No audio file provided"}), 400
audio = request.files["audio"]
source_lang = request.form.get("source_lang", None)
# Don't force language if "auto" is selected
if source_lang == "auto":
source_lang = None
temp_file = "temp_audio.wav"
try:
audio.save(temp_file)
result = speech.transcribe(temp_file, language=source_lang)
# Apply pre-translation corrections
if result.get("text"):
detected_lang = result.get("language", "en")
correction = corrector.pre_translate(result["text"], detected_lang)
result["corrected_text"] = correction["text"]
result["corrections"] = correction["corrections"]
result["correction_count"] = correction["correction_count"]
else:
result["corrected_text"] = ""
result["corrections"] = []
result["correction_count"] = 0
return jsonify(result)
except Exception as e:
traceback.print_exc()
return jsonify({"error": str(e)}), 500
finally:
if os.path.exists(temp_file):
try:
os.unlink(temp_file)
except OSError:
pass
# ── Translate ────────────────────────────────────────────────────────────────
@app.route("/api/translate", methods=["POST"])
def api_translate():
"""
Full translation pipeline:
Text β†’ Pre-correction β†’ Language Detection β†’ NLLB Translation
β†’ Post-correction β†’ Validation β†’ History β†’ Response
"""
data = request.get_json()
text = data.get("text", "").strip()
source_lang = data.get("source_lang", "auto")
target_lang = data.get("target_lang", "en")
if not text:
return jsonify({"error": "No text provided"}), 400
pipeline_start = time.time()
# 1. Detect source language if "auto"
if source_lang == "auto":
detection = detector.detect_from_text(text)
source_lang = detection["language"]
detection_confidence = detection["confidence"]
detection_method = detection["method"]
else:
detection_confidence = 1.0
detection_method = "manual"
# 2. Pre-translation correction
pre_result = corrector.pre_translate(text, source_lang)
corrected_text = pre_result["text"]
# 3. NLLB Translation
trans_result = translator.translate(corrected_text, source_lang, target_lang)
translated_text = trans_result.get("translated", corrected_text)
# 4. Post-translation correction
post_result = corrector.post_translate(translated_text, source_lang, target_lang)
final_text = post_result["text"]
# 5. Validate against datasets (if available)
validation = datasets.validate_translation(
text, final_text, source_lang, target_lang
)
total_time = round((time.time() - pipeline_start) * 1000)
# 6. Build response
result = {
"original": text,
"corrected": corrected_text,
"translated": final_text,
"source_lang": source_lang,
"source_lang_name": LANGUAGE_NAMES.get(source_lang, source_lang),
"target_lang": target_lang,
"target_lang_name": LANGUAGE_NAMES.get(target_lang, target_lang),
"timestamp": datetime.now().strftime("%H:%M:%S"),
"time_ms": total_time,
"translation_time_ms": trans_result.get("time_ms", 0),
"detection": {
"language": source_lang,
"confidence": detection_confidence,
"method": detection_method
},
"pre_corrections": pre_result["corrections"],
"post_corrections": post_result["corrections"],
"validation": validation,
"error": trans_result.get("error")
}
# 7. Save to history
history.add(result)
return jsonify(result)
# ── Text-to-Speech ───────────────────────────────────────────────────────────
@app.route("/api/tts", methods=["POST"])
def api_tts():
"""Convert text to speech using Piper (offline)."""
data = request.get_json()
text = data.get("text", "").strip()
lang_code = data.get("lang_code", "en")
if not text:
return jsonify({"error": "No text"}), 400
result = tts.synthesize(text, lang_code)
# Cleanup old files periodically
tts.cleanup_old_audio()
return jsonify(result)
@app.route("/api/tts/voices")
def api_tts_voices():
"""Return available TTS voice models."""
return jsonify(tts.get_stats())
# ── History ──────────────────────────────────────────────────────────────────
@app.route("/api/history")
def api_history():
limit = request.args.get("limit", 50, type=int)
return jsonify(history.get_all(limit))
@app.route("/api/history/stats")
def api_history_stats():
return jsonify(history.get_stats())
@app.route("/api/history/search")
def api_history_search():
query = request.args.get("q", "")
return jsonify(history.search(query))
@app.route("/api/clear", methods=["POST"])
def api_clear():
history.clear()
return jsonify({"success": True})
@app.route("/api/history/export")
def api_export_history():
"""Export history as JSON or CSV."""
fmt = request.args.get("format", "json")
if fmt == "csv":
csv_data = history.export_csv()
return Response(
csv_data,
mimetype="text/csv",
headers={"Content-Disposition": "attachment; filename=translation_history.csv"}
)
else:
json_data = history.export_json()
return Response(
json_data,
mimetype="application/json",
headers={"Content-Disposition": "attachment; filename=translation_history.json"}
)
# ── Corrections ──────────────────────────────────────────────────────────────
@app.route("/api/corrections/stats")
def api_corrections_stats():
return jsonify(corrector.get_stats())
@app.route("/api/corrections/add", methods=["POST"])
def api_add_correction():
"""Add a new correction entry."""
data = request.get_json()
success = corrector.add_correction(
category=data.get("category", "slang"),
wrong=data.get("wrong", ""),
correct=data.get("correct", ""),
domain=data.get("domain")
)
return jsonify({"success": success})
# ── Datasets ─────────────────────────────────────────────────────────────────
@app.route("/api/datasets/stats")
def api_datasets_stats():
return jsonify(datasets.get_stats())
# ═══════════════════════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
init_modules()
port = int(os.environ.get("PORT", 5000))
# Force port 7860 on Hugging Face Spaces (required by Gradio SDK)
if "SPACE_ID" in os.environ:
port = 7860
print(f"\n{'='*50}")
print(f" {APP_TITLE} {APP_VERSION}")
print(f" http://127.0.0.1:{port}")
print(f"{'='*50}\n")
app.run(host="0.0.0.0", port=port, debug=False)