Spaces:
Building
Building
Commit
Β·
4635155
0
Parent(s):
Initial commit of EAC Translator
Browse files- .gitignore +15 -0
- README.md +27 -0
- app.py +155 -0
- requirements.txt +9 -0
.gitignore
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Python cache
|
2 |
+
__pycache__/
|
3 |
+
*.pyc
|
4 |
+
|
5 |
+
# Audio output
|
6 |
+
*.wav
|
7 |
+
|
8 |
+
# Virtual environments
|
9 |
+
venv/
|
10 |
+
env/
|
11 |
+
opus-env/
|
12 |
+
|
13 |
+
# System files
|
14 |
+
.DS_Store
|
15 |
+
Thumbs.db
|
README.md
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# π EAC Translator
|
2 |
+
|
3 |
+
A multilingual translator for English, French, and Swahili with voice input and output. Built with β€οΈ by [Eng. Jobbers β Qtrinova Inc](https://eng-jobbers.vercel.app/).
|
4 |
+
|
5 |
+
## β¨ Features
|
6 |
+
|
7 |
+
- ποΈ Voice input via microphone
|
8 |
+
- π§ Automatic language detection
|
9 |
+
- π English β French β Swahili (with pivot logic)
|
10 |
+
- ποΈ Tone control (Neutral, Formal, Casual, Romantic)
|
11 |
+
- π Text-to-speech with voice selection (David, Zira, etc.)
|
12 |
+
- π± Mobile-friendly UI
|
13 |
+
- π€ Audio playback and download
|
14 |
+
|
15 |
+
## π Built With
|
16 |
+
|
17 |
+
- [Gradio](https://gradio.app)
|
18 |
+
- [Hugging Face Transformers](https://huggingface.co/transformers/)
|
19 |
+
- [pyttsx3](https://pypi.org/project/pyttsx3/)
|
20 |
+
- [SpeechRecognition](https://pypi.org/project/SpeechRecognition/)
|
21 |
+
- [langid](https://pypi.org/project/langid/)
|
22 |
+
|
23 |
+
## π¦ Installation (Optional for Local Use, especially VScode env setup, well tested)
|
24 |
+
|
25 |
+
```bash
|
26 |
+
pip install -r requirements.txt
|
27 |
+
python app.py
|
app.py
ADDED
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
from transformers import MarianMTModel, MarianTokenizer
|
3 |
+
from datetime import datetime
|
4 |
+
import langid
|
5 |
+
import os
|
6 |
+
import pyttsx3
|
7 |
+
import time
|
8 |
+
import warnings
|
9 |
+
|
10 |
+
warnings.filterwarnings("ignore", message="Recommended: pip install sacremoses.")
|
11 |
+
os.environ["PATH"] += os.pathsep + r"C:\ffmpeg\bin"
|
12 |
+
langid.set_languages(['en', 'fr', 'sw'])
|
13 |
+
|
14 |
+
MODEL_MAP = {
|
15 |
+
"English β Swahili": "Helsinki-NLP/opus-mt-en-sw",
|
16 |
+
"English β French": "Helsinki-NLP/opus-mt-en-fr",
|
17 |
+
"French β English": "Helsinki-NLP/opus-mt-fr-en",
|
18 |
+
"French β Swahili (via English)": ["Helsinki-NLP/opus-mt-fr-en", "Helsinki-NLP/opus-mt-en-sw"]
|
19 |
+
}
|
20 |
+
|
21 |
+
TONE_MODIFIERS = {
|
22 |
+
"Neutral": "",
|
23 |
+
"Romantic": "Express this romantically: ",
|
24 |
+
"Formal": "Translate this in a formal tone: ",
|
25 |
+
"Casual": "Make this sound casual: "
|
26 |
+
}
|
27 |
+
|
28 |
+
loaded_models = {}
|
29 |
+
|
30 |
+
def load_model(model_name):
|
31 |
+
if model_name not in loaded_models:
|
32 |
+
tokenizer = MarianTokenizer.from_pretrained(model_name)
|
33 |
+
model = MarianMTModel.from_pretrained(model_name)
|
34 |
+
loaded_models[model_name] = (tokenizer, model)
|
35 |
+
return loaded_models[model_name]
|
36 |
+
|
37 |
+
def detect_language(text):
|
38 |
+
try:
|
39 |
+
lang, score = langid.classify(text)
|
40 |
+
return lang
|
41 |
+
except:
|
42 |
+
return "unknown"
|
43 |
+
|
44 |
+
def translate(text, direction, tone):
|
45 |
+
detected_lang = detect_language(text)
|
46 |
+
expected_src = direction.split(" β ")[0].lower()
|
47 |
+
warning = ""
|
48 |
+
if expected_src.startswith("english") and detected_lang != "en":
|
49 |
+
warning = f"β οΈ Detected language is '{detected_lang}', but you selected English as source."
|
50 |
+
elif expected_src.startswith("french") and detected_lang != "fr":
|
51 |
+
warning = f"β οΈ Detected language is '{detected_lang}', but you selected French as source."
|
52 |
+
elif expected_src.startswith("swahili") and detected_lang != "sw":
|
53 |
+
warning = f"β οΈ Detected language is '{detected_lang}', but you selected Swahili as source."
|
54 |
+
|
55 |
+
prompt = TONE_MODIFIERS[tone] + text
|
56 |
+
model_info = MODEL_MAP[direction]
|
57 |
+
|
58 |
+
if isinstance(model_info, list):
|
59 |
+
tokenizer1, model1 = load_model(model_info[0])
|
60 |
+
encoded1 = tokenizer1(prompt, return_tensors="pt", padding=True, truncation=True)
|
61 |
+
intermediate = model1.generate(**encoded1)
|
62 |
+
intermediate_text = tokenizer1.decode(intermediate[0], skip_special_tokens=True)
|
63 |
+
|
64 |
+
tokenizer2, model2 = load_model(model_info[1])
|
65 |
+
encoded2 = tokenizer2(intermediate_text, return_tensors="pt", padding=True, truncation=True)
|
66 |
+
final = model2.generate(**encoded2)
|
67 |
+
translation = tokenizer2.decode(final[0], skip_special_tokens=True)
|
68 |
+
else:
|
69 |
+
tokenizer, model = load_model(model_info)
|
70 |
+
encoded = tokenizer(prompt, return_tensors="pt", padding=True, truncation=True)
|
71 |
+
generated = model.generate(**encoded)
|
72 |
+
translation = tokenizer.decode(generated[0], skip_special_tokens=True)
|
73 |
+
|
74 |
+
with open("translation_log.txt", "a", encoding="utf-8") as f:
|
75 |
+
f.write(f"[{datetime.now()}] {direction} | Tone: {tone}\n")
|
76 |
+
f.write(f"Input: {text}\nOutput: {translation}\n\n")
|
77 |
+
|
78 |
+
return f"{warning}\n{translation}" if warning else translation
|
79 |
+
|
80 |
+
engine = pyttsx3.init()
|
81 |
+
voices = engine.getProperty('voices')
|
82 |
+
voice_names = [voice.name for voice in voices]
|
83 |
+
|
84 |
+
def speak_text_to_file(text, voice_name):
|
85 |
+
try:
|
86 |
+
engine = pyttsx3.init()
|
87 |
+
engine.setProperty('rate', 150)
|
88 |
+
for voice in voices:
|
89 |
+
if voice.name == voice_name:
|
90 |
+
engine.setProperty('voice', voice.id)
|
91 |
+
break
|
92 |
+
output_path = "tts_output.wav"
|
93 |
+
engine.save_to_file(text, output_path)
|
94 |
+
engine.runAndWait()
|
95 |
+
return output_path
|
96 |
+
except Exception:
|
97 |
+
return None
|
98 |
+
|
99 |
+
def transcribe_and_translate(audio_path, direction, tone):
|
100 |
+
import speech_recognition as sr
|
101 |
+
recognizer = sr.Recognizer()
|
102 |
+
try:
|
103 |
+
with sr.AudioFile(audio_path) as source:
|
104 |
+
audio = recognizer.record(source)
|
105 |
+
if len(audio.frame_data) < 10000:
|
106 |
+
return "β οΈ Audio too short or empty. Please try again."
|
107 |
+
text = recognizer.recognize_google(audio)
|
108 |
+
return translate(text, direction, tone)
|
109 |
+
except Exception as e:
|
110 |
+
return f"β οΈ Could not transcribe audio: {e}"
|
111 |
+
|
112 |
+
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
113 |
+
gr.Markdown("## π EAC Translator")
|
114 |
+
gr.Markdown("Supports English, French, and Swahili. Includes tone control, language detection, voice input, and speech playback.")
|
115 |
+
|
116 |
+
with gr.Tabs():
|
117 |
+
with gr.Tab("π Text Translation"):
|
118 |
+
with gr.Column():
|
119 |
+
input_text = gr.Textbox(label="Text to Translate", lines=3)
|
120 |
+
direction = gr.Dropdown(choices=list(MODEL_MAP.keys()), label="Translation Direction", value="English β Swahili")
|
121 |
+
tone = gr.Radio(choices=list(TONE_MODIFIERS.keys()), label="Tone", value="Neutral")
|
122 |
+
output_text = gr.Textbox(label="Translated Text", lines=3)
|
123 |
+
voice_choice = gr.Dropdown(choices=voice_names, label="Voice for Playback", value=voice_names[0])
|
124 |
+
with gr.Row():
|
125 |
+
translate_btn = gr.Button("Translate", scale=1)
|
126 |
+
speak_btn = gr.Button("π Listen to Translation", scale=1)
|
127 |
+
audio_output = gr.Audio(label="Playback", interactive=False)
|
128 |
+
|
129 |
+
with gr.Tab("ποΈ Voice Translation"):
|
130 |
+
with gr.Column():
|
131 |
+
audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Speak Now")
|
132 |
+
direction_voice = gr.Dropdown(choices=list(MODEL_MAP.keys()), label="Translation Direction", value="English β Swahili")
|
133 |
+
tone_voice = gr.Radio(choices=list(TONE_MODIFIERS.keys()), label="Tone", value="Neutral")
|
134 |
+
voice_output = gr.Textbox(label="Translated Text")
|
135 |
+
voice_choice2 = gr.Dropdown(choices=voice_names, label="Voice for Playback", value=voice_names[0])
|
136 |
+
with gr.Row():
|
137 |
+
voice_translate_btn = gr.Button("Transcribe & Translate", scale=1)
|
138 |
+
voice_speak_btn = gr.Button("π Listen to Translation", scale=1)
|
139 |
+
audio_output2 = gr.Audio(label="Playback", interactive=False)
|
140 |
+
|
141 |
+
translate_btn.click(fn=translate, inputs=[input_text, direction, tone], outputs=output_text)
|
142 |
+
speak_btn.click(fn=speak_text_to_file, inputs=[output_text, voice_choice], outputs=audio_output)
|
143 |
+
voice_translate_btn.click(fn=transcribe_and_translate, inputs=[audio_input, direction_voice, tone_voice], outputs=voice_output)
|
144 |
+
voice_speak_btn.click(fn=speak_text_to_file, inputs=[voice_output, voice_choice2], outputs=audio_output2)
|
145 |
+
|
146 |
+
gr.Markdown(
|
147 |
+
"""<div style='text-align: center;'>
|
148 |
+
<a href='https://eng-jobbers.vercel.app/' target='_blank' style='text-decoration: none; font-weight: bold;'>
|
149 |
+
By Eng. Jobbers β Qtrinova Inc. NLPβ€οΈ
|
150 |
+
</a>
|
151 |
+
</div>""",
|
152 |
+
elem_id="footer"
|
153 |
+
)
|
154 |
+
|
155 |
+
demo.launch()
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio==5.35.0
|
2 |
+
transformers==4.53.0
|
3 |
+
torch==2.7.1
|
4 |
+
sentencepiece==0.2.0
|
5 |
+
langid
|
6 |
+
speechrecognition
|
7 |
+
pydub
|
8 |
+
pyttsx3
|
9 |
+
sacremoses
|