Spaces:
Running
on
Zero
Running
on
Zero
Commit
·
e102b16
1
Parent(s):
8b2823e
init
Browse files- .gitattributes +1 -0
- .gitignore +0 -1
- Dockerfile +28 -0
- README.md +5 -5
- app.py +0 -285
- assets/chatbot.jpg +0 -0
- assets/user.jpg +0 -0
- inference/__init__.py +0 -3
- inference/mistral.py +0 -53
- inference/nllb.py +0 -98
- inference/parle_tts.py +0 -197
- requirements.txt +0 -10
- styles.css +0 -172
- utils.py +0 -13
.gitattributes
CHANGED
@@ -18,6 +18,7 @@
|
|
18 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
*.pickle filter=lfs diff=lfs merge=lfs -text
|
|
|
21 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
22 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
23 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
|
|
18 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
.gitignore
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
.idea
|
|
|
|
Dockerfile
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.11-slim
|
2 |
+
|
3 |
+
|
4 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
5 |
+
git \
|
6 |
+
&& apt-get clean \
|
7 |
+
&& rm -rf /var/lib/apt/lists/*
|
8 |
+
|
9 |
+
RUN useradd -m -u 1000 user
|
10 |
+
USER user
|
11 |
+
ENV HOME="/home/user"
|
12 |
+
ENV PATH="${HOME}/.local/bin:$PATH"
|
13 |
+
WORKDIR $HOME/app
|
14 |
+
|
15 |
+
|
16 |
+
|
17 |
+
# --- ✅ Cloner le repo privé avec authentification
|
18 |
+
# --- https://huggingface.co/docs/hub/en/spaces-sdks-docker#secrets-and-variables-management
|
19 |
+
RUN --mount=type=secret,id=GITHUB_TOKEN,mode=0444,required=true \
|
20 |
+
git clone https://sawadogosalif:$(cat /run/secrets/GITHUB_TOKEN)@github.com/BurkimbIA/spaces.git
|
21 |
+
|
22 |
+
|
23 |
+
RUN cp -r leaderboards/mt/* .
|
24 |
+
|
25 |
+
RUN pip install -r requirements.txt
|
26 |
+
|
27 |
+
|
28 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
@@ -1,12 +1,12 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
colorFrom: red
|
5 |
colorTo: green
|
6 |
-
sdk:
|
7 |
-
sdk_version: 5.34.2
|
8 |
app_file: app.py
|
9 |
pinned: false
|
|
|
10 |
---
|
11 |
|
12 |
-
|
|
|
1 |
---
|
2 |
+
title: Moore MT Leaderboard
|
3 |
+
emoji: 🚗
|
4 |
colorFrom: red
|
5 |
colorTo: green
|
6 |
+
sdk: docker
|
|
|
7 |
app_file: app.py
|
8 |
pinned: false
|
9 |
+
short_description: Text2text Machine Translation for Moore language
|
10 |
---
|
11 |
|
12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
DELETED
@@ -1,285 +0,0 @@
|
|
1 |
-
import re
|
2 |
-
import gradio as gr
|
3 |
-
import spaces
|
4 |
-
from inference import NLLBTranslator, MistralTranslator, ParlerTTSGenerator
|
5 |
-
from utils import load_css
|
6 |
-
from loguru import logger
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
translator_nllb = NLLBTranslator()
|
12 |
-
translator_mistral = MistralTranslator("sawadogosalif/SaChi_by_Mistral")
|
13 |
-
tts_generator = ParlerTTSGenerator("burkimbia/BIA-ParlerTTS-mini")
|
14 |
-
|
15 |
-
@spaces.GPU(duration=15)
|
16 |
-
def translate_text(message, model_choice, src_lang, tgt_lang):
|
17 |
-
"""Fonction de traduction"""
|
18 |
-
|
19 |
-
if model_choice == "SaChi-MT0.5 (NLLB)":
|
20 |
-
translator = translator_nllb
|
21 |
-
else:
|
22 |
-
translator = translator_mistral
|
23 |
-
|
24 |
-
return translator.translate(
|
25 |
-
text=message,
|
26 |
-
src_lang=src_lang,
|
27 |
-
tgt_lang=tgt_lang
|
28 |
-
)
|
29 |
-
|
30 |
-
@spaces.GPU(duration=15)
|
31 |
-
def tts_speech(text, speaker_choice):
|
32 |
-
"""Fonction de génération de la synthèse vocale"""
|
33 |
-
|
34 |
-
sample_rate, audio_arr = tts_generator.generate_speech(
|
35 |
-
text,
|
36 |
-
speaker_type=speaker_choice
|
37 |
-
)
|
38 |
-
|
39 |
-
if audio_arr is not None:
|
40 |
-
return sample_rate, audio_arr
|
41 |
-
return None
|
42 |
-
|
43 |
-
@spaces.GPU(duration=30)
|
44 |
-
def response_with_tts(message, history, model_choice, direction_choice, enable_tts, speaker_choice):
|
45 |
-
"""Fonction de réponse avec traduction et TTS optionnel"""
|
46 |
-
|
47 |
-
if direction_choice == "French to Moore":
|
48 |
-
src_lang, tgt_lang = "fra_Latn", "moor_Latn"
|
49 |
-
else:
|
50 |
-
src_lang, tgt_lang = "moor_Latn", "fra_Latn"
|
51 |
-
|
52 |
-
try:
|
53 |
-
translated_text = translate_text(message, model_choice, src_lang, tgt_lang)
|
54 |
-
except Exception as e:
|
55 |
-
logger.error(f"Erreur de traduction: {str(e)}")
|
56 |
-
return f"Erreur de traduction: {str(e)}", None
|
57 |
-
|
58 |
-
# Génération TTS si activée et si on traduit vers le mooré
|
59 |
-
audio_output = None
|
60 |
-
if enable_tts and direction_choice == "French to Moore" and translated_text:
|
61 |
-
try:
|
62 |
-
logger.info("Génération de la synthèse vocale...")
|
63 |
-
tts_result = tts_speech(translated_text, speaker_choice)
|
64 |
-
|
65 |
-
if tts_result is not None:
|
66 |
-
audio_output = tts_result
|
67 |
-
logger.info("Synthèse vocale générée avec succès")
|
68 |
-
else:
|
69 |
-
logger.warning("Échec de la génération TTS")
|
70 |
-
|
71 |
-
except Exception as e:
|
72 |
-
logger.error(f"Erreur TTS: {str(e)}")
|
73 |
-
|
74 |
-
return translated_text, audio_output
|
75 |
-
|
76 |
-
|
77 |
-
# Thème personnalisé
|
78 |
-
theme = gr.themes.Soft(
|
79 |
-
primary_hue=gr.themes.colors.purple,
|
80 |
-
secondary_hue=gr.themes.colors.blue,
|
81 |
-
neutral_hue=gr.themes.colors.slate,
|
82 |
-
font=gr.themes.GoogleFont("Inter"),
|
83 |
-
radius_size=gr.themes.sizes.radius_lg,
|
84 |
-
)
|
85 |
-
|
86 |
-
with gr.Blocks(theme=theme, css=load_css(), title="SaChi Multi-Modèles Demo") as demo:
|
87 |
-
|
88 |
-
# En-tête principal
|
89 |
-
with gr.Row(elem_classes="main-container"):
|
90 |
-
with gr.Column():
|
91 |
-
gr.HTML("""
|
92 |
-
<div class="header-section">
|
93 |
-
<h1 style="margin: 0; font-size: 2.5em; font-weight: 700;">
|
94 |
-
🎯 SaChi Multi-Modèles Demo avec Synthèse Vocale
|
95 |
-
</h1>
|
96 |
-
<div style="margin-top: 20px;">
|
97 |
-
<div class="feature-item" style="justify-content: center;">
|
98 |
-
<span class="feature-icon">🔄</span>
|
99 |
-
<span>Traduction bidirectionnelle Français ↔ Mooré</span>
|
100 |
-
</div>
|
101 |
-
<div class="feature-item" style="justify-content: center;">
|
102 |
-
<span class="feature-icon">🗣️</span>
|
103 |
-
<span>Synthèse vocale en mooré avec différentes voix</span>
|
104 |
-
</div>
|
105 |
-
</div>
|
106 |
-
<div style="margin-top: 25px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.2);">
|
107 |
-
<div style="display: flex; justify-content: center; gap: 30px; flex-wrap: wrap;">
|
108 |
-
<div class="model-item" style="color: rgba(255,255,255,0.9);">
|
109 |
-
<span class="feature-icon">📝</span>
|
110 |
-
<strong>Traduction:</strong> SaChi-MT0.5 (NLLB) / SaChi-MT1.5 (Mistral-Instruct)
|
111 |
-
</div>
|
112 |
-
<div class="model-item" style="color: rgba(255,255,255,0.9);">
|
113 |
-
<span class="feature-icon">🎤</span>
|
114 |
-
<strong>TTS:</strong> BIA-ParlerTTS-mini (mooré uniquement)
|
115 |
-
</div>
|
116 |
-
</div>
|
117 |
-
</div>
|
118 |
-
</div>
|
119 |
-
""")
|
120 |
-
|
121 |
-
# Interface principale
|
122 |
-
with gr.Row(elem_classes="main-container equal-height-row"):
|
123 |
-
# Colonne de gauche - Configuration
|
124 |
-
with gr.Column(scale=1, elem_classes="equal-height-column"):
|
125 |
-
with gr.Column(elem_classes="config-card"):
|
126 |
-
gr.HTML('<h2 class="section-title"><span class="section-icon">⚙️</span>Configuration</h2>')
|
127 |
-
|
128 |
-
model_choice = gr.Dropdown(
|
129 |
-
choices=["SaChi-MT0.5 (NLLB)", "SaChi-MT1.5 (Mistral-Instruct)"],
|
130 |
-
label="🤖 Modèle de traduction",
|
131 |
-
value="SaChi-MT1.5 (Mistral-Instruct)",
|
132 |
-
info="Choisissez le modèle de traduction",
|
133 |
-
container=True
|
134 |
-
)
|
135 |
-
|
136 |
-
direction_choice = gr.Dropdown(
|
137 |
-
choices=["French to Moore", "Moore to French"],
|
138 |
-
label="🔄 Direction de traduction",
|
139 |
-
value="French to Moore",
|
140 |
-
info="Sens de la traduction",
|
141 |
-
container=True
|
142 |
-
)
|
143 |
-
|
144 |
-
gr.HTML('<hr style="margin: 20px 0; border: none; border-top: 1px solid #e2e8f0;">')
|
145 |
-
|
146 |
-
enable_tts = gr.Checkbox(
|
147 |
-
label="🎤 Activer la synthèse vocale",
|
148 |
-
value=True,
|
149 |
-
info="Uniquement pour français → mooré"
|
150 |
-
)
|
151 |
-
|
152 |
-
speaker_choice = gr.Dropdown(
|
153 |
-
choices=["default", "male", "female"],
|
154 |
-
label="👤 Type de voix",
|
155 |
-
value="default",
|
156 |
-
info="Type de locuteur pour le TTS",
|
157 |
-
visible=True,
|
158 |
-
container=True
|
159 |
-
)
|
160 |
-
|
161 |
-
# Colonne de droite - Interface de traduction
|
162 |
-
with gr.Column(scale=2, elem_classes="equal-height-column"):
|
163 |
-
with gr.Column(elem_classes="translation-card"):
|
164 |
-
gr.HTML('<h2 class="section-title"><span class="section-icon">💬</span>Traduction</h2>')
|
165 |
-
|
166 |
-
text_input = gr.Textbox(
|
167 |
-
label="📝 Texte à traduire",
|
168 |
-
placeholder="Ex: Bonjour, comment allez-vous ?",
|
169 |
-
value="Bonjour, comment allez-vous ?",
|
170 |
-
lines=4,
|
171 |
-
max_lines=8,
|
172 |
-
container=True
|
173 |
-
)
|
174 |
-
|
175 |
-
with gr.Row():
|
176 |
-
translate_btn = gr.Button(
|
177 |
-
"🚀 Traduire",
|
178 |
-
variant="primary",
|
179 |
-
size="lg",
|
180 |
-
elem_classes="button-primary"
|
181 |
-
)
|
182 |
-
clear_btn = gr.Button(
|
183 |
-
"🗑️ Effacer",
|
184 |
-
variant="secondary",
|
185 |
-
elem_classes="button-secondary"
|
186 |
-
)
|
187 |
-
|
188 |
-
text_output = gr.Textbox(
|
189 |
-
label="📄 Traduction",
|
190 |
-
lines=4,
|
191 |
-
max_lines=8,
|
192 |
-
interactive=False,
|
193 |
-
container=True
|
194 |
-
)
|
195 |
-
|
196 |
-
generate_audio_btn = gr.Button(
|
197 |
-
"🎤 Générer l'audio",
|
198 |
-
variant="secondary",
|
199 |
-
size="lg",
|
200 |
-
visible=True,
|
201 |
-
elem_classes="button-secondary"
|
202 |
-
)
|
203 |
-
|
204 |
-
audio_output = gr.Audio(
|
205 |
-
label="🔊 Synthèse vocale (mooré)",
|
206 |
-
visible=True,
|
207 |
-
autoplay=False
|
208 |
-
)
|
209 |
-
|
210 |
-
# Fonctions de traitement
|
211 |
-
def translate_only(message, model_choice, direction_choice):
|
212 |
-
if direction_choice == "French to Moore":
|
213 |
-
src_lang, tgt_lang = "fra_Latn", "moor_Latn"
|
214 |
-
else:
|
215 |
-
src_lang, tgt_lang = "moor_Latn", "fra_Latn"
|
216 |
-
try:
|
217 |
-
translated_text = translate_text(message, model_choice, src_lang, tgt_lang)
|
218 |
-
return translated_text, gr.update(visible=direction_choice == "French to Moore")
|
219 |
-
except Exception as e:
|
220 |
-
logger.error(f"Erreur de traduction: {str(e)}")
|
221 |
-
return f"Erreur de traduction: {str(e)}", gr.update(visible=False)
|
222 |
-
|
223 |
-
def generate_audio(translated_text, speaker_choice):
|
224 |
-
try:
|
225 |
-
logger.info("Génération de la synthèse vocale...")
|
226 |
-
tts_result = tts_speech(translated_text, speaker_choice)
|
227 |
-
if tts_result is not None:
|
228 |
-
logger.info("Synthèse vocale générée avec succès")
|
229 |
-
return tts_result
|
230 |
-
else:
|
231 |
-
logger.warning("Échec de la génération TTS")
|
232 |
-
return None
|
233 |
-
except Exception as e:
|
234 |
-
logger.error(f"Erreur TTS: {str(e)}")
|
235 |
-
return None
|
236 |
-
|
237 |
-
def clear_fields():
|
238 |
-
return (
|
239 |
-
"Bonjour, comment allez-vous ?",
|
240 |
-
"",
|
241 |
-
None,
|
242 |
-
gr.update(visible=True)
|
243 |
-
)
|
244 |
-
|
245 |
-
def toggle_tts_options(direction, tts_enabled):
|
246 |
-
show_tts = (direction == "French to Moore" and tts_enabled)
|
247 |
-
return {
|
248 |
-
speaker_choice: gr.update(visible=show_tts),
|
249 |
-
audio_output: gr.update(visible=show_tts),
|
250 |
-
generate_audio_btn: gr.update(visible=show_tts)
|
251 |
-
}
|
252 |
-
|
253 |
-
# Événements
|
254 |
-
translate_btn.click(
|
255 |
-
fn=translate_only,
|
256 |
-
inputs=[text_input, model_choice, direction_choice],
|
257 |
-
outputs=[text_output, generate_audio_btn]
|
258 |
-
)
|
259 |
-
|
260 |
-
generate_audio_btn.click(
|
261 |
-
fn=generate_audio,
|
262 |
-
inputs=[text_output, speaker_choice],
|
263 |
-
outputs=[audio_output]
|
264 |
-
)
|
265 |
-
|
266 |
-
clear_btn.click(
|
267 |
-
fn=clear_fields,
|
268 |
-
inputs=[],
|
269 |
-
outputs=[text_input, text_output, audio_output, generate_audio_btn]
|
270 |
-
)
|
271 |
-
|
272 |
-
direction_choice.change(
|
273 |
-
fn=lambda d, t: toggle_tts_options(d, t),
|
274 |
-
inputs=[direction_choice, enable_tts],
|
275 |
-
outputs=[speaker_choice, audio_output, generate_audio_btn]
|
276 |
-
)
|
277 |
-
|
278 |
-
enable_tts.change(
|
279 |
-
fn=lambda t, d: toggle_tts_options(d, t),
|
280 |
-
inputs=[enable_tts, direction_choice],
|
281 |
-
outputs=[speaker_choice, audio_output, generate_audio_btn]
|
282 |
-
)
|
283 |
-
|
284 |
-
if __name__ == "__main__":
|
285 |
-
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/chatbot.jpg
DELETED
Binary file (61.2 kB)
|
|
assets/user.jpg
DELETED
Binary file (2.66 kB)
|
|
inference/__init__.py
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
from .mistral import MistralTranslator
|
2 |
-
from .nllb import NLLBTranslator
|
3 |
-
from .parle_tts import ParlerTTSGenerator
|
|
|
|
|
|
|
|
inference/mistral.py
DELETED
@@ -1,53 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
import re
|
3 |
-
from peft import AutoPeftModelForCausalLM
|
4 |
-
from transformers import AutoTokenizer
|
5 |
-
|
6 |
-
|
7 |
-
class MistralTranslator:
|
8 |
-
"""
|
9 |
-
Wrapper around a Mistral-Instruct-based model fine-tuned for French↔Mooré translation.
|
10 |
-
"""
|
11 |
-
def __init__(self, model_id: str, hf_token_env: str = "HF_TOKEN"):
|
12 |
-
hf_token = os.environ.get(hf_token_env)
|
13 |
-
if hf_token is None:
|
14 |
-
raise ValueError(f"Please set the environment variable {hf_token_env} with your HuggingFace token.")
|
15 |
-
self.model = AutoPeftModelForCausalLM.from_pretrained(
|
16 |
-
model_id,
|
17 |
-
token=hf_token,
|
18 |
-
device_map="auto"
|
19 |
-
)
|
20 |
-
self.tokenizer = AutoTokenizer.from_pretrained(model_id)
|
21 |
-
self.prompt_template = (
|
22 |
-
"""
|
23 |
-
<s>
|
24 |
-
|
25 |
-
You are an expert Moore translator. Translate the provided {src_name} text to {tgt_name}.
|
26 |
-
The Moore alphabet is: a, ã, b, d, e, ẽ, ɛ, f, g, h, i, ĩ, ɩ, k, l, m, n, o, õ, p, r, s, t, u, ũ, ʋ, v, w, y, z.
|
27 |
-
Based on source language ({src_name}), provide the {tgt_name} text.
|
28 |
-
[INST]
|
29 |
-
### {src_name}:
|
30 |
-
{text}
|
31 |
-
[/INST]
|
32 |
-
|
33 |
-
### {tgt_name}:
|
34 |
-
"""
|
35 |
-
)
|
36 |
-
|
37 |
-
def translate(self, text: str, src_lang: str, tgt_lang: str) -> str:
|
38 |
-
lang_map = {"fra_Latn": "French", "moor_Latn": "Moore"}
|
39 |
-
src_name = lang_map.get(src_lang, src_lang)
|
40 |
-
tgt_name = lang_map.get(tgt_lang, tgt_lang)
|
41 |
-
prompt = self.prompt_template.format(src_name=src_name, tgt_name=tgt_name, text=text)
|
42 |
-
inputs = self.tokenizer(prompt, return_tensors="pt").to("cuda")
|
43 |
-
outputs = self.model.generate(
|
44 |
-
input_ids=inputs.input_ids,
|
45 |
-
attention_mask=inputs.attention_mask,
|
46 |
-
max_new_tokens=512,
|
47 |
-
do_sample=False
|
48 |
-
)
|
49 |
-
decoded = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
|
50 |
-
pattern = rf"### {tgt_name}:\s*(.+)"
|
51 |
-
match = re.search(pattern, decoded, re.DOTALL)
|
52 |
-
return match.group(1).strip() if match else decoded
|
53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
inference/nllb.py
DELETED
@@ -1,98 +0,0 @@
|
|
1 |
-
import re
|
2 |
-
import os
|
3 |
-
import sys
|
4 |
-
import typing as tp
|
5 |
-
import unicodedata
|
6 |
-
|
7 |
-
import torch
|
8 |
-
from sacremoses import MosesPunctNormalizer
|
9 |
-
from transformers import AutoModelForSeq2SeqLM, NllbTokenizer
|
10 |
-
|
11 |
-
MODEL_URL = "sawadogosalif/SaChi-MT"
|
12 |
-
|
13 |
-
|
14 |
-
from huggingface_hub import login
|
15 |
-
auth_token = os.getenv('HF_TOKEN')
|
16 |
-
login(token=auth_token)
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
class TextPreprocessor:
|
23 |
-
"""
|
24 |
-
Mimic the text preprocessing made for the NLLB model.
|
25 |
-
This code is adapted from the Stopes repo of the NLLB team:
|
26 |
-
https://github.com/facebookresearch/stopes/blob/main/stopes/pipelines/monolingual/monolingual_line_processor.py#L214
|
27 |
-
"""
|
28 |
-
|
29 |
-
def __init__(self, lang="fr"):
|
30 |
-
self.mpn = MosesPunctNormalizer(lang=lang)
|
31 |
-
self.mpn.substitutions = [
|
32 |
-
(re.compile(r), sub) for r, sub in self.mpn.substitutions
|
33 |
-
]
|
34 |
-
|
35 |
-
def __call__(self, text: str) -> str:
|
36 |
-
clean = self.mpn.normalize(text)
|
37 |
-
clean = unicodedata.normalize("NFKC", clean)
|
38 |
-
clean = clean[0].lower() + clean[1:]
|
39 |
-
return clean
|
40 |
-
|
41 |
-
|
42 |
-
def fix_tokenizer(tokenizer, new_lang):
|
43 |
-
"""
|
44 |
-
Ajoute un nouveau token de langue au tokenizer et met à jour les mappings d’identifiants.
|
45 |
-
|
46 |
-
- Ajoute le token spécial s'il n'existe pas déjà.
|
47 |
-
- Initialise ou met à jour `lang_code_to_id` et `id_to_lang_code` en utilisant `getattr` pour éviter les vérifications répétitives.
|
48 |
-
"""
|
49 |
-
if new_lang not in tokenizer.additional_special_tokens:
|
50 |
-
tokenizer.add_special_tokens({'additional_special_tokens': [new_lang]})
|
51 |
-
|
52 |
-
tokenizer.lang_code_to_id = getattr(tokenizer, 'lang_code_to_id', {})
|
53 |
-
tokenizer.id_to_lang_code = getattr(tokenizer, 'id_to_lang_code', {})
|
54 |
-
|
55 |
-
if new_lang not in tokenizer.lang_code_to_id:
|
56 |
-
new_lang_id = tokenizer.convert_tokens_to_ids(new_lang)
|
57 |
-
tokenizer.lang_code_to_id[new_lang] = new_lang_id
|
58 |
-
tokenizer.id_to_lang_code[new_lang_id] = new_lang
|
59 |
-
|
60 |
-
return tokenizer
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
class NLLBTranslator:
|
65 |
-
def __init__(self):
|
66 |
-
self.model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_URL)
|
67 |
-
if torch.cuda.is_available():
|
68 |
-
self.model.cuda()
|
69 |
-
self.tokenizer = NllbTokenizer.from_pretrained(MODEL_URL)
|
70 |
-
self.tokenizer = fix_tokenizer(self.tokenizer, "moor_Latn")
|
71 |
-
|
72 |
-
self.preprocessor = TextPreprocessor()
|
73 |
-
|
74 |
-
def translate(self, text, src_lang='fr_Latn', tgt_lang='moor_Latn', a=32, b=3, max_input_length=1024, num_beams=5, **kwargs):
|
75 |
-
# 🩹 temporary
|
76 |
-
tmp = tgt_lang
|
77 |
-
src_lang = tgt_lang
|
78 |
-
tgt_lang = tmp
|
79 |
-
|
80 |
-
|
81 |
-
self.tokenizer.src_lang = src_lang
|
82 |
-
self.tokenizer.tgt_lang = tgt_lang
|
83 |
-
text_clean = self.preprocessor(text)
|
84 |
-
|
85 |
-
inputs = self.tokenizer(text_clean, return_tensors='pt', padding=True, truncation=True, max_length=max_input_length)
|
86 |
-
result = self.model.generate(
|
87 |
-
**inputs.to(self.model.device),
|
88 |
-
forced_bos_token_id=self.tokenizer.convert_tokens_to_ids(tgt_lang),
|
89 |
-
max_new_tokens=int(a + b * inputs.input_ids.shape[1]),
|
90 |
-
num_beams=num_beams,
|
91 |
-
**kwargs
|
92 |
-
)
|
93 |
-
output = self.tokenizer.batch_decode(result, skip_special_tokens=True)[0]
|
94 |
-
if text.endswith('?') or text.endswith('!'):
|
95 |
-
output += text[-1] # Ajouter le dernier caractère (soit "?" ou "!")
|
96 |
-
|
97 |
-
|
98 |
-
return output.capitalize()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
inference/parle_tts.py
DELETED
@@ -1,197 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
import torch
|
3 |
-
import numpy as np
|
4 |
-
from transformers import AutoTokenizer
|
5 |
-
from parler_tts import ParlerTTSForConditionalGeneration
|
6 |
-
from loguru import logger
|
7 |
-
|
8 |
-
from huggingface_hub import login
|
9 |
-
auth_token = os.getenv('HF_TOKEN')
|
10 |
-
login(token=auth_token)
|
11 |
-
|
12 |
-
|
13 |
-
class ParlerTTSGenerator:
|
14 |
-
"""Générateur TTS pour le mooré utilisant ParlerTTS"""
|
15 |
-
|
16 |
-
def __init__(self, model_checkpoint="burkimbia/BIA-ParlerTTS-mini"):
|
17 |
-
"""
|
18 |
-
Initialiser le générateur TTS
|
19 |
-
|
20 |
-
Args:
|
21 |
-
model_checkpoint (str): Nom du modèle HuggingFace
|
22 |
-
"""
|
23 |
-
self.model_checkpoint = model_checkpoint
|
24 |
-
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
25 |
-
self.model = None
|
26 |
-
self.tokenizer = None
|
27 |
-
self.description_tokenizer = None
|
28 |
-
|
29 |
-
# Configuration par défaut pour la génération
|
30 |
-
self.gen_kwargs = {
|
31 |
-
'do_sample': True,
|
32 |
-
'temperature': 1.0,
|
33 |
-
'max_length': 2580,
|
34 |
-
'min_new_tokens': 10
|
35 |
-
}
|
36 |
-
|
37 |
-
# Descriptions des locuteurs par défaut
|
38 |
-
self.speaker_descriptions = {
|
39 |
-
"default": "Christian speaks very slowly but has an animated delivery in a room with background",
|
40 |
-
"male": "A male voice speaking in Moore language with calm intonation and clear pronunciation.",
|
41 |
-
"female": "A female voice speaking in Moore language with gentle tone and clear articulation."
|
42 |
-
}
|
43 |
-
|
44 |
-
self.current_speaker = "default"
|
45 |
-
|
46 |
-
def load_model(self):
|
47 |
-
"""Charger le modèle TTS de manière lazy"""
|
48 |
-
try:
|
49 |
-
if self.model is None:
|
50 |
-
logger.info(f"Chargement du modèle TTS: {self.model_checkpoint}")
|
51 |
-
|
52 |
-
self.model = ParlerTTSForConditionalGeneration.from_pretrained(
|
53 |
-
self.model_checkpoint,
|
54 |
-
use_auth_token=True, # Pour les repos privés
|
55 |
-
torch_dtype=torch.float16 if self.device == "cuda" else torch.float32
|
56 |
-
).to(self.device)
|
57 |
-
|
58 |
-
self.tokenizer = AutoTokenizer.from_pretrained(
|
59 |
-
self.model_checkpoint,
|
60 |
-
use_auth_token=True
|
61 |
-
)
|
62 |
-
|
63 |
-
self.description_tokenizer = AutoTokenizer.from_pretrained(
|
64 |
-
self.model_checkpoint,
|
65 |
-
use_auth_token=True
|
66 |
-
)
|
67 |
-
|
68 |
-
logger.info("Modèle TTS chargé avec succès")
|
69 |
-
|
70 |
-
except Exception as e:
|
71 |
-
logger.error(f"Erreur lors du chargement du modèle TTS: {str(e)}")
|
72 |
-
raise
|
73 |
-
|
74 |
-
def set_speaker(self, speaker_type="default"):
|
75 |
-
"""
|
76 |
-
Définir le type de locuteur
|
77 |
-
|
78 |
-
Args:
|
79 |
-
speaker_type (str): Type de locuteur ("default", "male", "female")
|
80 |
-
"""
|
81 |
-
if speaker_type in self.speaker_descriptions:
|
82 |
-
self.current_speaker = speaker_type
|
83 |
-
else:
|
84 |
-
logger.warning(f"Type de locuteur '{speaker_type}' non reconnu, utilisation de 'default'")
|
85 |
-
self.current_speaker = "default"
|
86 |
-
|
87 |
-
def set_custom_description(self, description):
|
88 |
-
"""
|
89 |
-
Définir une description personnalisée pour le locuteur
|
90 |
-
|
91 |
-
Args:
|
92 |
-
description (str): Description personnalisée du locuteur
|
93 |
-
"""
|
94 |
-
self.speaker_descriptions["custom"] = description
|
95 |
-
self.current_speaker = "custom"
|
96 |
-
|
97 |
-
def generate_speech(self, text, speaker_type=None):
|
98 |
-
"""
|
99 |
-
Générer la parole à partir du texte
|
100 |
-
|
101 |
-
Args:
|
102 |
-
text (str): Texte à synthétiser
|
103 |
-
speaker_type (str, optional): Type de locuteur à utiliser
|
104 |
-
|
105 |
-
Returns:
|
106 |
-
tuple: (sample_rate, audio_array) ou (None, None) en cas d'erreur
|
107 |
-
"""
|
108 |
-
try:
|
109 |
-
if self.model is None:
|
110 |
-
self.load_model()
|
111 |
-
|
112 |
-
if speaker_type:
|
113 |
-
self.set_speaker(speaker_type)
|
114 |
-
|
115 |
-
speaker_description = self.speaker_descriptions[self.current_speaker]
|
116 |
-
|
117 |
-
text = self._preprocess_text(text)
|
118 |
-
|
119 |
-
input_ids = self.description_tokenizer(
|
120 |
-
speaker_description,
|
121 |
-
return_tensors="pt",
|
122 |
-
padding=True,
|
123 |
-
truncation=True
|
124 |
-
).input_ids.to(self.device)
|
125 |
-
|
126 |
-
prompt_input_ids = self.tokenizer(
|
127 |
-
text,
|
128 |
-
return_tensors="pt",
|
129 |
-
padding=True,
|
130 |
-
truncation=True
|
131 |
-
).input_ids.to(self.device)
|
132 |
-
|
133 |
-
logger.info(f"Génération TTS pour: '{text[:50]}...'")
|
134 |
-
|
135 |
-
with torch.no_grad():
|
136 |
-
generation = self.model.generate(
|
137 |
-
input_ids=input_ids,
|
138 |
-
prompt_input_ids=prompt_input_ids,
|
139 |
-
**self.gen_kwargs
|
140 |
-
)
|
141 |
-
|
142 |
-
audio_arr = generation.cpu().numpy().squeeze()
|
143 |
-
sample_rate = self.model.config.sampling_rate
|
144 |
-
|
145 |
-
logger.info("Synthèse vocale réussie")
|
146 |
-
return sample_rate, audio_arr
|
147 |
-
|
148 |
-
except Exception as e:
|
149 |
-
logger.error(f"Erreur lors de la synthèse vocale: {str(e)}")
|
150 |
-
return None, None
|
151 |
-
|
152 |
-
def _preprocess_text(self, text):
|
153 |
-
"""
|
154 |
-
Préprocesser le texte avant la synthèse
|
155 |
-
|
156 |
-
Args:
|
157 |
-
text (str): Texte original
|
158 |
-
|
159 |
-
Returns:
|
160 |
-
str: Texte préprocessé
|
161 |
-
"""
|
162 |
-
# Nettoyer le texte si nécessaire
|
163 |
-
text = text.strip()
|
164 |
-
# to improve
|
165 |
-
return text
|
166 |
-
|
167 |
-
def is_model_loaded(self):
|
168 |
-
"""Vérifier si le modèle est chargé"""
|
169 |
-
return self.model is not None
|
170 |
-
|
171 |
-
def unload_model(self):
|
172 |
-
"""Décharger le modèle pour libérer la mémoire"""
|
173 |
-
if self.model is not None:
|
174 |
-
del self.model
|
175 |
-
del self.tokenizer
|
176 |
-
del self.description_tokenizer
|
177 |
-
self.model = None
|
178 |
-
self.tokenizer = None
|
179 |
-
self.description_tokenizer = None
|
180 |
-
|
181 |
-
if torch.cuda.is_available():
|
182 |
-
torch.cuda.empty_cache()
|
183 |
-
|
184 |
-
logger.info("Modèle TTS déchargé")
|
185 |
-
|
186 |
-
def get_available_speakers(self):
|
187 |
-
"""Obtenir la liste des types de locuteurs disponibles"""
|
188 |
-
return list(self.speaker_descriptions.keys())
|
189 |
-
|
190 |
-
def get_model_info(self):
|
191 |
-
"""Obtenir des informations sur le modèle"""
|
192 |
-
return {
|
193 |
-
"model_checkpoint": self.model_checkpoint,
|
194 |
-
"device": self.device,
|
195 |
-
"is_loaded": self.is_model_loaded(),
|
196 |
-
"available_speakers": self.get_available_speakers()
|
197 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
DELETED
@@ -1,10 +0,0 @@
|
|
1 |
-
accelerate==1.6.0
|
2 |
-
bitsandbytes==0.45.5
|
3 |
-
peft==0.15.0
|
4 |
-
gradio>=3.18.0
|
5 |
-
torch
|
6 |
-
loguru
|
7 |
-
sacremoses
|
8 |
-
sentencepiece
|
9 |
-
spaces
|
10 |
-
git+https://github.com/huggingface/parler-tts.git
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
styles.css
DELETED
@@ -1,172 +0,0 @@
|
|
1 |
-
|
2 |
-
.main-container {
|
3 |
-
max-width: 1200px;
|
4 |
-
margin: 0 auto;
|
5 |
-
padding: 20px;
|
6 |
-
}
|
7 |
-
|
8 |
-
.header-section {
|
9 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
10 |
-
color: white;
|
11 |
-
padding: 30px;
|
12 |
-
border-radius: 15px;
|
13 |
-
margin-bottom: 30px;
|
14 |
-
text-align: center;
|
15 |
-
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
16 |
-
}
|
17 |
-
|
18 |
-
.config-card {
|
19 |
-
background: white;
|
20 |
-
border-radius: 15px;
|
21 |
-
padding: 25px;
|
22 |
-
box-shadow: 0 5px 20px rgba(0,0,0,0.08);
|
23 |
-
border: 1px solid #e2e8f0;
|
24 |
-
margin-bottom: 20px;
|
25 |
-
height: 100%;
|
26 |
-
display: flex;
|
27 |
-
flex-direction: column;
|
28 |
-
}
|
29 |
-
|
30 |
-
.translation-card {
|
31 |
-
background: white;
|
32 |
-
border-radius: 15px;
|
33 |
-
padding: 25px;
|
34 |
-
box-shadow: 0 5px 20px rgba(0,0,0,0.08);
|
35 |
-
border: 1px solid #e2e8f0;
|
36 |
-
height: 100%;
|
37 |
-
display: flex;
|
38 |
-
flex-direction: column;
|
39 |
-
}
|
40 |
-
|
41 |
-
/* Assurer que les colonnes ont la même hauteur */
|
42 |
-
.equal-height-row {
|
43 |
-
display: flex;
|
44 |
-
align-items: stretch;
|
45 |
-
}
|
46 |
-
|
47 |
-
.equal-height-column {
|
48 |
-
display: flex;
|
49 |
-
flex-direction: column;
|
50 |
-
}
|
51 |
-
|
52 |
-
.feature-item {
|
53 |
-
display: flex;
|
54 |
-
align-items: center;
|
55 |
-
margin: 10px 0;
|
56 |
-
font-size: 16px;
|
57 |
-
}
|
58 |
-
|
59 |
-
.feature-icon {
|
60 |
-
margin-right: 10px;
|
61 |
-
font-size: 18px;
|
62 |
-
}
|
63 |
-
|
64 |
-
.model-item {
|
65 |
-
display: flex;
|
66 |
-
align-items: center;
|
67 |
-
margin: 8px 0;
|
68 |
-
font-size: 14px;
|
69 |
-
color: #64748b;
|
70 |
-
}
|
71 |
-
|
72 |
-
.section-title {
|
73 |
-
font-size: 20px;
|
74 |
-
font-weight: 600;
|
75 |
-
margin-bottom: 20px;
|
76 |
-
color: #1e293b;
|
77 |
-
display: flex;
|
78 |
-
align-items: center;
|
79 |
-
}
|
80 |
-
|
81 |
-
.section-icon {
|
82 |
-
margin-right: 10px;
|
83 |
-
font-size: 22px;
|
84 |
-
}
|
85 |
-
|
86 |
-
.button-primary {
|
87 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
88 |
-
border: none !important;
|
89 |
-
color: white !important;
|
90 |
-
font-weight: 600 !important;
|
91 |
-
padding: 12px 24px !important;
|
92 |
-
border-radius: 8px !important;
|
93 |
-
transition: all 0.3s ease !important;
|
94 |
-
}
|
95 |
-
|
96 |
-
.button-primary:hover {
|
97 |
-
transform: translateY(-2px) !important;
|
98 |
-
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3) !important;
|
99 |
-
}
|
100 |
-
|
101 |
-
.button-secondary {
|
102 |
-
background: #f8fafc !important;
|
103 |
-
border: 2px solid #e2e8f0 !important;
|
104 |
-
color: #64748b !important;
|
105 |
-
font-weight: 600 !important;
|
106 |
-
padding: 12px 24px !important;
|
107 |
-
border-radius: 8px !important;
|
108 |
-
transition: all 0.3s ease !important;
|
109 |
-
}
|
110 |
-
|
111 |
-
.button-secondary:hover {
|
112 |
-
background: #f1f5f9 !important;
|
113 |
-
border-color: #cbd5e1 !important;
|
114 |
-
}
|
115 |
-
|
116 |
-
/* Styles responsifs */
|
117 |
-
@media (max-width: 768px) {
|
118 |
-
.main-container {
|
119 |
-
padding: 10px;
|
120 |
-
}
|
121 |
-
|
122 |
-
.header-section {
|
123 |
-
padding: 20px;
|
124 |
-
}
|
125 |
-
|
126 |
-
.config-card,
|
127 |
-
.translation-card {
|
128 |
-
padding: 20px;
|
129 |
-
}
|
130 |
-
|
131 |
-
.equal-height-row {
|
132 |
-
flex-direction: column;
|
133 |
-
}
|
134 |
-
}
|
135 |
-
|
136 |
-
/* Amélioration des composants Gradio */
|
137 |
-
.gradio-container {
|
138 |
-
font-family: 'Inter', sans-serif;
|
139 |
-
}
|
140 |
-
|
141 |
-
/* Styling pour les dropdowns */
|
142 |
-
.gr-dropdown {
|
143 |
-
border-radius: 8px !important;
|
144 |
-
border: 1px solid #e2e8f0 !important;
|
145 |
-
}
|
146 |
-
|
147 |
-
/* Styling pour les textboxes */
|
148 |
-
.gr-textbox {
|
149 |
-
border-radius: 8px !important;
|
150 |
-
border: 1px solid #e2e8f0 !important;
|
151 |
-
}
|
152 |
-
|
153 |
-
/* Styling pour les checkboxes */
|
154 |
-
.gr-checkbox {
|
155 |
-
border-radius: 4px !important;
|
156 |
-
}
|
157 |
-
|
158 |
-
/* Animation de chargement */
|
159 |
-
.loading-spinner {
|
160 |
-
display: inline-block;
|
161 |
-
width: 20px;
|
162 |
-
height: 20px;
|
163 |
-
border: 3px solid #f3f3f3;
|
164 |
-
border-top: 3px solid #667eea;
|
165 |
-
border-radius: 50%;
|
166 |
-
animation: spin 1s linear infinite;
|
167 |
-
}
|
168 |
-
|
169 |
-
@keyframes spin {
|
170 |
-
0% { transform: rotate(0deg); }
|
171 |
-
100% { transform: rotate(360deg); }
|
172 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils.py
DELETED
@@ -1,13 +0,0 @@
|
|
1 |
-
|
2 |
-
import os
|
3 |
-
|
4 |
-
|
5 |
-
def load_css():
|
6 |
-
"""Charge le fichier CSS externe"""
|
7 |
-
css_path = os.path.join(os.path.dirname(__file__), "styles.css")
|
8 |
-
try:
|
9 |
-
with open(css_path, "r", encoding="utf-8") as f:
|
10 |
-
return f.read()
|
11 |
-
except FileNotFoundError:
|
12 |
-
logger.warning(f"Fichier CSS non trouvé: {css_path}")
|
13 |
-
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|