Spaces:
Running
Running
Upload 21 files
Browse files- web/__pycache__/urls.cpython-310.pyc +0 -0
- web/__pycache__/views.cpython-310.pyc +0 -0
- web/templates/web/chat.html +71 -4
- web/templates/web/chat_settings.html +16 -2
- web/templates/web/new_chat.html +16 -1
- web/templates/web/settings.html +18 -0
- web/urls.py +1 -0
- web/views.py +258 -12
web/__pycache__/urls.cpython-310.pyc
CHANGED
|
Binary files a/web/__pycache__/urls.cpython-310.pyc and b/web/__pycache__/urls.cpython-310.pyc differ
|
|
|
web/__pycache__/views.cpython-310.pyc
CHANGED
|
Binary files a/web/__pycache__/views.cpython-310.pyc and b/web/__pycache__/views.cpython-310.pyc differ
|
|
|
web/templates/web/chat.html
CHANGED
|
@@ -39,6 +39,7 @@
|
|
| 39 |
<button class="action-btn" onclick="handleAction('translate', {{ msg.id }})">🇪🇺 Translate</button>
|
| 40 |
<button class="action-btn" onclick="handleAction('word_to_word', {{ msg.id }})">🔤 Literal</button>
|
| 41 |
<button class="action-btn" onclick="handleAction('grammatical_explanation', {{ msg.id }})">🎭 Style Analysis</button>
|
|
|
|
| 42 |
{% endif %}
|
| 43 |
</div>
|
| 44 |
</div>
|
|
@@ -62,7 +63,14 @@
|
|
| 62 |
font-size: 0.88rem; display: none; line-height: 1.6; border: 1px solid var(--glass-border);
|
| 63 |
box-shadow: inset 0 2px 10px rgba(0,0,0,0.2);
|
| 64 |
animation: slideUp 0.4s cubic-bezier(0, 0.5, 0.5, 1);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
|
|
|
|
|
|
|
|
|
| 66 |
@keyframes slideUp {
|
| 67 |
from { opacity: 0; transform: translateY(15px) scale(0.98); }
|
| 68 |
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
@@ -125,12 +133,17 @@ async function handleAction(action, msgId) {
|
|
| 125 |
const data = await response.json();
|
| 126 |
if (response.ok) {
|
| 127 |
if (data.is_structured && data.result.words) {
|
| 128 |
-
let tableHtml = '
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
data.result.words.forEach(row => {
|
| 130 |
tableHtml += `<tr>
|
| 131 |
-
<td>${row.form || ''}</td>
|
| 132 |
-
<td>${row.
|
| 133 |
-
<td>${row.
|
|
|
|
| 134 |
</tr>`;
|
| 135 |
});
|
| 136 |
tableHtml += '</tbody></table>';
|
|
@@ -327,6 +340,60 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 327 |
}, 3000);
|
| 328 |
}
|
| 329 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
// Se l'ultimo messaggio è dell'utente, iniziamo a pollarne la risposta
|
| 331 |
const allMsgs = document.querySelectorAll('.message-wrapper');
|
| 332 |
if (allMsgs.length > 0 && allMsgs[allMsgs.length-1].classList.contains('user')) {
|
|
|
|
| 39 |
<button class="action-btn" onclick="handleAction('translate', {{ msg.id }})">🇪🇺 Translate</button>
|
| 40 |
<button class="action-btn" onclick="handleAction('word_to_word', {{ msg.id }})">🔤 Literal</button>
|
| 41 |
<button class="action-btn" onclick="handleAction('grammatical_explanation', {{ msg.id }})">🎭 Style Analysis</button>
|
| 42 |
+
<button class="action-btn" onclick="speakText({{ msg.id }}, event)">🔊 Listen</button>
|
| 43 |
{% endif %}
|
| 44 |
</div>
|
| 45 |
</div>
|
|
|
|
| 63 |
font-size: 0.88rem; display: none; line-height: 1.6; border: 1px solid var(--glass-border);
|
| 64 |
box-shadow: inset 0 2px 10px rgba(0,0,0,0.2);
|
| 65 |
animation: slideUp 0.4s cubic-bezier(0, 0.5, 0.5, 1);
|
| 66 |
+
max-height: 400px;
|
| 67 |
+
overflow-y: auto;
|
| 68 |
+
scrollbar-width: thin;
|
| 69 |
+
scrollbar-color: var(--secondary) rgba(255,255,255,0.05);
|
| 70 |
}
|
| 71 |
+
.action-result::-webkit-scrollbar { width: 6px; }
|
| 72 |
+
.action-result::-webkit-scrollbar-track { background: rgba(255,255,255,0.05); border-radius: 10px; }
|
| 73 |
+
.action-result::-webkit-scrollbar-thumb { background: var(--secondary); border-radius: 10px; }
|
| 74 |
@keyframes slideUp {
|
| 75 |
from { opacity: 0; transform: translateY(15px) scale(0.98); }
|
| 76 |
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
|
|
| 133 |
const data = await response.json();
|
| 134 |
if (response.ok) {
|
| 135 |
if (data.is_structured && data.result.words) {
|
| 136 |
+
let tableHtml = '';
|
| 137 |
+
if (data.source) {
|
| 138 |
+
tableHtml += `<div style="font-size: 0.65rem; color: var(--text-dim); margin-bottom: 8px; opacity: 0.7; text-transform: uppercase; font-weight: 800; letter-spacing: 0.05em;">🛠️ DEBUG Source: ${data.source}</div>`;
|
| 139 |
+
}
|
| 140 |
+
tableHtml += '<table><thead><tr><th>Form</th><th>Translation</th><th>Lemma</th><th>Morphology</th></tr></thead><tbody>';
|
| 141 |
data.result.words.forEach(row => {
|
| 142 |
tableHtml += `<tr>
|
| 143 |
+
<td><b style="color: var(--text-main);">${row.form || ''}</b></td>
|
| 144 |
+
<td style="font-weight: 500; color: var(--secondary);">${row.translation || ''}</td>
|
| 145 |
+
<td><i>${row.lemma || ''}</i></td>
|
| 146 |
+
<td style="font-size: 0.65rem; color: var(--text-dim);">${row.grammar_comments || ''}</td>
|
| 147 |
</tr>`;
|
| 148 |
});
|
| 149 |
tableHtml += '</tbody></table>';
|
|
|
|
| 340 |
}, 3000);
|
| 341 |
}
|
| 342 |
|
| 343 |
+
window.speakText = async function(messageId, event) {
|
| 344 |
+
const textElement = document.getElementById(`msg-${messageId}`);
|
| 345 |
+
if (!textElement) return;
|
| 346 |
+
const text = textElement.innerText.trim();
|
| 347 |
+
|
| 348 |
+
const btn = event.currentTarget;
|
| 349 |
+
const originalText = btn.innerHTML;
|
| 350 |
+
btn.innerHTML = "⌛...";
|
| 351 |
+
btn.disabled = true;
|
| 352 |
+
|
| 353 |
+
try {
|
| 354 |
+
const response = await fetch('{% url "tts_proxy" %}', {
|
| 355 |
+
method: 'POST',
|
| 356 |
+
headers: {
|
| 357 |
+
'Content-Type': 'application/json',
|
| 358 |
+
'X-CSRFToken': getCookie('csrftoken')
|
| 359 |
+
},
|
| 360 |
+
body: JSON.stringify({
|
| 361 |
+
text: text,
|
| 362 |
+
lang: "{{ chat.language }}"
|
| 363 |
+
})
|
| 364 |
+
});
|
| 365 |
+
|
| 366 |
+
if (response.ok) {
|
| 367 |
+
const blob = await response.blob();
|
| 368 |
+
const url = window.URL.createObjectURL(blob);
|
| 369 |
+
const audio = new Audio(url);
|
| 370 |
+
|
| 371 |
+
btn.innerHTML = "🔊 Playing";
|
| 372 |
+
audio.play();
|
| 373 |
+
|
| 374 |
+
audio.onended = () => {
|
| 375 |
+
window.URL.revokeObjectURL(url);
|
| 376 |
+
btn.innerHTML = originalText;
|
| 377 |
+
btn.disabled = false;
|
| 378 |
+
};
|
| 379 |
+
audio.onerror = () => {
|
| 380 |
+
console.error("Audio playback error");
|
| 381 |
+
btn.innerHTML = "❌ Error";
|
| 382 |
+
setTimeout(() => { btn.innerHTML = originalText; btn.disabled = false; }, 2000);
|
| 383 |
+
};
|
| 384 |
+
} else {
|
| 385 |
+
const errData = await response.json();
|
| 386 |
+
alert("TTS Error: " + errData.error);
|
| 387 |
+
btn.innerHTML = originalText;
|
| 388 |
+
btn.disabled = false;
|
| 389 |
+
}
|
| 390 |
+
} catch (error) {
|
| 391 |
+
console.error("TTS Fetch Error:", error);
|
| 392 |
+
btn.innerHTML = "❌ Error";
|
| 393 |
+
setTimeout(() => { btn.innerHTML = originalText; btn.disabled = false; }, 2000);
|
| 394 |
+
}
|
| 395 |
+
};
|
| 396 |
+
|
| 397 |
// Se l'ultimo messaggio è dell'utente, iniziamo a pollarne la risposta
|
| 398 |
const allMsgs = document.querySelectorAll('.message-wrapper');
|
| 399 |
if (allMsgs.length > 0 && allMsgs[allMsgs.length-1].classList.contains('user')) {
|
web/templates/web/chat_settings.html
CHANGED
|
@@ -17,8 +17,22 @@
|
|
| 17 |
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.9rem;">📝 Persona Bio / Behavior</label>
|
| 18 |
<textarea name="bio" rows="4" placeholder="AI behavior for this chat..." required>{{ chat.character_bio }}</textarea>
|
| 19 |
|
| 20 |
-
<
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.9rem;">🛠️ MCP Tool Sources (Web/SSE)</label>
|
| 24 |
<div id="mcp-container" style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px;">
|
|
|
|
| 17 |
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.9rem;">📝 Persona Bio / Behavior</label>
|
| 18 |
<textarea name="bio" rows="4" placeholder="AI behavior for this chat..." required>{{ chat.character_bio }}</textarea>
|
| 19 |
|
| 20 |
+
<select name="lang" required style="width: 100%; padding: 14px; background: rgba(0,0,0,0.7); border: 1px solid var(--glass-border); border-radius: 12px; color: white; margin-bottom: 20px; transition: all 0.2s; font-size: 1rem;">
|
| 21 |
+
<optgroup label="🌍 Official EU Languages" style="background: #0a0a14; color: #fff;">
|
| 22 |
+
{% for lang in supported_languages %}
|
| 23 |
+
{% if not lang.small %}
|
| 24 |
+
<option value="{{ lang.name }}" {% if lang.name == chat.language %}selected{% endif %}>{{ lang.flag }} {{ lang.name }} ({{ lang.code }})</option>
|
| 25 |
+
{% endif %}
|
| 26 |
+
{% endfor %}
|
| 27 |
+
</optgroup>
|
| 28 |
+
<optgroup label="🏛️ Regional & Small Languages" style="background: #0a0a14; color: #fff;">
|
| 29 |
+
{% for lang in supported_languages %}
|
| 30 |
+
{% if lang.small %}
|
| 31 |
+
<option value="{{ lang.name }}" {% if lang.name == chat.language %}selected{% endif %}>{{ lang.flag }} {{ lang.name }} ({{ lang.code }})</option>
|
| 32 |
+
{% endif %}
|
| 33 |
+
{% endfor %}
|
| 34 |
+
</optgroup>
|
| 35 |
+
</select>
|
| 36 |
|
| 37 |
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.9rem;">🛠️ MCP Tool Sources (Web/SSE)</label>
|
| 38 |
<div id="mcp-container" style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px;">
|
web/templates/web/new_chat.html
CHANGED
|
@@ -15,7 +15,22 @@
|
|
| 15 |
<input type="text" name="name" placeholder="e.g. Italian Coffee Break" required>
|
| 16 |
|
| 17 |
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.9rem;">🌍 Target Language</label>
|
| 18 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.9rem;">👤 Tutor Persona / Bio</label>
|
| 21 |
<textarea name="bio" rows="4" placeholder="e.g. A friendly Milanese chef..." required>{{ profile.global_bio }}</textarea>
|
|
|
|
| 15 |
<input type="text" name="name" placeholder="e.g. Italian Coffee Break" required>
|
| 16 |
|
| 17 |
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.9rem;">🌍 Target Language</label>
|
| 18 |
+
<select name="lang" required style="width: 100%; padding: 14px; background: rgba(0,0,0,0.7); border: 1px solid var(--glass-border); border-radius: 12px; color: white; cursor: pointer; transition: all 0.23s; font-size: 1rem;">
|
| 19 |
+
<optgroup label="🌍 Official EU Languages" style="background: #0a0a14; color: #fff;">
|
| 20 |
+
{% for lang in supported_languages %}
|
| 21 |
+
{% if not lang.small %}
|
| 22 |
+
<option value="{{ lang.name }}" {% if lang.name == profile.global_language %}selected{% endif %}>{{ lang.flag }} {{ lang.name }} ({{ lang.code }})</option>
|
| 23 |
+
{% endif %}
|
| 24 |
+
{% endfor %}
|
| 25 |
+
</optgroup>
|
| 26 |
+
<optgroup label="🏛️ Regional & Small Languages" style="background: #0a0a14; color: #fff;">
|
| 27 |
+
{% for lang in supported_languages %}
|
| 28 |
+
{% if lang.small %}
|
| 29 |
+
<option value="{{ lang.name }}" {% if lang.name == profile.global_language %}selected{% endif %}>{{ lang.flag }} {{ lang.name }} ({{ lang.code }})</option>
|
| 30 |
+
{% endif %}
|
| 31 |
+
{% endfor %}
|
| 32 |
+
</optgroup>
|
| 33 |
+
</select>
|
| 34 |
|
| 35 |
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.9rem;">👤 Tutor Persona / Bio</label>
|
| 36 |
<textarea name="bio" rows="4" placeholder="e.g. A friendly Milanese chef..." required>{{ profile.global_bio }}</textarea>
|
web/templates/web/settings.html
CHANGED
|
@@ -23,6 +23,24 @@
|
|
| 23 |
|
| 24 |
<button type="button" id="add-mcp" class="btn btn-secondary" style="margin-bottom: 20px; font-size: 0.8rem;">➕ Add Tool Source</button>
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 1rem;">⏳ Message Bundle Timer (seconds)</label>
|
| 27 |
<input type="number" name="global_timer" value="{{ profile.global_timer }}" min="1" max="120" style="margin-bottom: 20px;">
|
| 28 |
|
|
|
|
| 23 |
|
| 24 |
<button type="button" id="add-mcp" class="btn btn-secondary" style="margin-bottom: 20px; font-size: 0.8rem;">➕ Add Tool Source</button>
|
| 25 |
|
| 26 |
+
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 1rem;">🌍 Global Default Language</label>
|
| 27 |
+
<select name="global_language" style="margin-bottom: 20px; width: 100%; padding: 14px; background: rgba(0,0,0,0.7); border: 1px solid var(--glass-border); border-radius: 12px; color: white;">
|
| 28 |
+
<optgroup label="🌍 Official EU Languages" style="background: #0a0a14; color: #fff;">
|
| 29 |
+
{% for lang in supported_languages %}
|
| 30 |
+
{% if not lang.small %}
|
| 31 |
+
<option value="{{ lang.name }}" {% if lang.name == profile.global_language %}selected{% endif %}>{{ lang.flag }} {{ lang.name }} ({{ lang.code }})</option>
|
| 32 |
+
{% endif %}
|
| 33 |
+
{% endfor %}
|
| 34 |
+
</optgroup>
|
| 35 |
+
<optgroup label="🏛️ Regional & Small Languages" style="background: #0a0a14; color: #fff;">
|
| 36 |
+
{% for lang in supported_languages %}
|
| 37 |
+
{% if lang.small %}
|
| 38 |
+
<option value="{{ lang.name }}" {% if lang.name == profile.global_language %}selected{% endif %}>{{ lang.flag }} {{ lang.name }} ({{ lang.code }})</option>
|
| 39 |
+
{% endif %}
|
| 40 |
+
{% endfor %}
|
| 41 |
+
</optgroup>
|
| 42 |
+
</select>
|
| 43 |
+
|
| 44 |
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 1rem;">⏳ Message Bundle Timer (seconds)</label>
|
| 45 |
<input type="number" name="global_timer" value="{{ profile.global_timer }}" min="1" max="120" style="margin-bottom: 20px;">
|
| 46 |
|
web/urls.py
CHANGED
|
@@ -16,5 +16,6 @@ urlpatterns = [
|
|
| 16 |
path('settings/', views.global_settings, name='global_settings'),
|
| 17 |
path('chat/<int:chat_id>/ai-action/', views.ai_action, name='ai_action'),
|
| 18 |
path('webhook/telegram/', views.telegram_webhook, name='telegram_webhook'),
|
|
|
|
| 19 |
|
| 20 |
]
|
|
|
|
| 16 |
path('settings/', views.global_settings, name='global_settings'),
|
| 17 |
path('chat/<int:chat_id>/ai-action/', views.ai_action, name='ai_action'),
|
| 18 |
path('webhook/telegram/', views.telegram_webhook, name='telegram_webhook'),
|
| 19 |
+
path('tts/', views.tts_proxy, name='tts_proxy'),
|
| 20 |
|
| 21 |
]
|
web/views.py
CHANGED
|
@@ -16,6 +16,37 @@ from langchain_core.output_parsers import JsonOutputParser
|
|
| 16 |
from pydantic import BaseModel, Field
|
| 17 |
from typing import List
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
class WordAnalysis(BaseModel):
|
| 20 |
form: str = Field(description="The word as it appears in the text")
|
| 21 |
grammar_comments: str = Field(description="Grammatical comments about the word")
|
|
@@ -72,6 +103,11 @@ llm_util = HuggingFaceEndpoint(
|
|
| 72 |
)
|
| 73 |
chat_model_util = ChatHuggingFace(llm=llm_util)
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
# Dizionario globale per gestire i timer dei messaggi (per chat_id)
|
| 76 |
active_timers = {}
|
| 77 |
|
|
@@ -269,7 +305,10 @@ def create_new_chat(request):
|
|
| 269 |
)
|
| 270 |
return redirect('chat_detail', chat_id=chat.id)
|
| 271 |
|
| 272 |
-
return render(request, 'web/new_chat.html', {
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
|
| 275 |
@csrf_exempt
|
|
@@ -297,7 +336,8 @@ def chat_settings(request, chat_id):
|
|
| 297 |
|
| 298 |
|
| 299 |
return render(request, 'web/chat_settings.html', {
|
| 300 |
-
'chat': chat
|
|
|
|
| 301 |
})
|
| 302 |
|
| 303 |
|
|
@@ -326,11 +366,13 @@ def global_settings(request):
|
|
| 326 |
mcp_list = request.POST.getlist('mcp_tools')
|
| 327 |
profile.global_mcp_tools = [url.strip() for url in mcp_list if url.strip()]
|
| 328 |
profile.global_timer = int(request.POST.get('global_timer', profile.global_timer))
|
|
|
|
| 329 |
profile.save()
|
| 330 |
return redirect('index')
|
| 331 |
|
| 332 |
return render(request, 'web/settings.html', {
|
| 333 |
-
'profile': profile
|
|
|
|
| 334 |
})
|
| 335 |
|
| 336 |
|
|
@@ -363,7 +405,13 @@ def ai_action(request, chat_id):
|
|
| 363 |
chat = get_object_or_404(ChatSession, id=chat_id, user=user)
|
| 364 |
|
| 365 |
target_lang = chat.language
|
|
|
|
|
|
|
|
|
|
| 366 |
|
|
|
|
|
|
|
|
|
|
| 367 |
# Prompt logic based on bot copy.py
|
| 368 |
if action == "grammar_check":
|
| 369 |
prompt = (
|
|
@@ -386,16 +434,131 @@ def ai_action(request, chat_id):
|
|
| 386 |
elif action == "translate_to_target":
|
| 387 |
prompt = f"Translate the following English text into {target_lang}. Provide ONLY the translation without any introduction.\n\nText: {text_to_analyze}."
|
| 388 |
elif action == "word_to_word":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
prompt = (
|
| 390 |
f"Provide a word-for-word literal translation of the following text into English (or {target_lang} if it is already in English). "
|
| 391 |
-
f"Format the output as a
|
| 392 |
-
f"In the 'Form' column, include the original word from the text. In the 'Grammar Comments' column, include brief grammatical comments about the word (e.g., gender, case, person, tense). In the 'Lemma' column, include the dictionary form of the word. In the 'Translation' column, include the English translation of the word. Do not add any additional information."
|
| 393 |
f"\n\nText: {text_to_analyze}"
|
| 394 |
)
|
| 395 |
elif action == "grammatical_explanation":
|
| 396 |
prompt = (
|
| 397 |
f"Provide a stylistic analysis of the following text in {target_lang}. "
|
| 398 |
-
f"For each sentence, explain the stylistic choices, why it was said in that way, and identify if it uses colloquialisms, slang, or dialect. "
|
| 399 |
f"\n\nText: {text_to_analyze}"
|
| 400 |
)
|
| 401 |
else:
|
|
@@ -410,7 +573,7 @@ def ai_action(request, chat_id):
|
|
| 410 |
SystemMessage(content="You are a highly capable linguistic assistant that always responds in valid JSON format as requested."),
|
| 411 |
HumanMessage(content=full_prompt)
|
| 412 |
]
|
| 413 |
-
response_ai =
|
| 414 |
try:
|
| 415 |
ai_text = parser.parse(response_ai.content)
|
| 416 |
is_structured = True
|
|
@@ -423,7 +586,7 @@ def ai_action(request, chat_id):
|
|
| 423 |
SystemMessage(content="You are a highly capable linguistic assistant that always responds in valid JSON format as requested."),
|
| 424 |
HumanMessage(content=full_prompt)
|
| 425 |
]
|
| 426 |
-
response_ai =
|
| 427 |
try:
|
| 428 |
ai_text = parser.parse(response_ai.content)
|
| 429 |
is_structured = True
|
|
@@ -436,7 +599,7 @@ def ai_action(request, chat_id):
|
|
| 436 |
SystemMessage(content="You are a linguistic expert assistant. Always respond in JSON format."),
|
| 437 |
HumanMessage(content=full_prompt)
|
| 438 |
]
|
| 439 |
-
response_ai =
|
| 440 |
try:
|
| 441 |
ai_text = parser.parse(response_ai.content)
|
| 442 |
is_structured = True
|
|
@@ -449,21 +612,72 @@ def ai_action(request, chat_id):
|
|
| 449 |
SystemMessage(content="You are a native speaker linguistic assistant. Always respond in JSON format."),
|
| 450 |
HumanMessage(content=full_prompt)
|
| 451 |
]
|
| 452 |
-
response_ai =
|
| 453 |
try:
|
| 454 |
ai_text = parser.parse(response_ai.content)
|
| 455 |
is_structured = True
|
| 456 |
except Exception:
|
| 457 |
ai_text = response_ai.content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
else:
|
| 459 |
messages = [
|
| 460 |
SystemMessage(content="You are a highly capable linguistic assistant."),
|
| 461 |
HumanMessage(content=prompt)
|
| 462 |
]
|
| 463 |
-
response_ai =
|
| 464 |
ai_text = response_ai.content
|
| 465 |
|
| 466 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
except Exception as e:
|
| 468 |
return JsonResponse({'error': str(e)}, status=500)
|
| 469 |
|
|
@@ -529,3 +743,35 @@ def process_chat_reply(chat):
|
|
| 529 |
except Exception as e:
|
| 530 |
print(f"Error AI: {e}")
|
| 531 |
PendingMessage.objects.create(chat=chat, content="Sorry, I'm having trouble thinking right now.", is_user=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
from pydantic import BaseModel, Field
|
| 17 |
from typing import List
|
| 18 |
|
| 19 |
+
SUPPORTED_LANGUAGES = [
|
| 20 |
+
{'name': 'Bulgarian', 'code': 'bg', 'small': True, 'regional': False, 'flag': '🇧🇬'},
|
| 21 |
+
{'name': 'Croatian', 'code': 'hr', 'small': True, 'regional': False, 'flag': '🇭🇷'},
|
| 22 |
+
{'name': 'Czech', 'code': 'cs', 'small': True, 'regional': False, 'flag': '🇨🇿'},
|
| 23 |
+
{'name': 'Danish', 'code': 'da', 'small': True, 'regional': False, 'flag': '🇩🇰'},
|
| 24 |
+
{'name': 'Dutch', 'code': 'nl', 'small': False, 'regional': False, 'flag': '🇳🇱'},
|
| 25 |
+
{'name': 'English', 'code': 'en', 'small': False, 'regional': False, 'flag': '🇬🇧'},
|
| 26 |
+
{'name': 'Estonian', 'code': 'et', 'small': True, 'regional': False, 'flag': '🇪🇪'},
|
| 27 |
+
{'name': 'Finnish', 'code': 'fi', 'small': True, 'regional': False, 'flag': '🇫🇮'},
|
| 28 |
+
{'name': 'French', 'code': 'fr', 'small': False, 'regional': False, 'flag': '🇫🇷'},
|
| 29 |
+
{'name': 'German', 'code': 'de', 'small': False, 'regional': False, 'flag': '🇩🇪'},
|
| 30 |
+
{'name': 'Greek', 'code': 'el', 'small': True, 'regional': False, 'flag': '🇬🇷'},
|
| 31 |
+
{'name': 'Hungarian', 'code': 'hu', 'small': True, 'regional': False, 'flag': '🇭🇺'},
|
| 32 |
+
{'name': 'Irish', 'code': 'ga', 'small': True, 'regional': False, 'flag': '🇮🇪'},
|
| 33 |
+
{'name': 'Italian', 'code': 'it', 'small': False, 'regional': False, 'flag': '🇮🇹'},
|
| 34 |
+
{'name': 'Latvian', 'code': 'lv', 'small': True, 'regional': False, 'flag': '🇱🇻'},
|
| 35 |
+
{'name': 'Lithuanian', 'code': 'lt', 'small': True, 'regional': False, 'flag': '🇱🇹'},
|
| 36 |
+
{'name': 'Maltese', 'code': 'mt', 'small': True, 'regional': False, 'flag': '🇲🇹'},
|
| 37 |
+
{'name': 'Polish', 'code': 'pl', 'small': False, 'regional': False, 'flag': '🇵🇱'},
|
| 38 |
+
{'name': 'Portuguese', 'code': 'pt', 'small': False, 'regional': False, 'flag': '🇵🇹'},
|
| 39 |
+
{'name': 'Romanian', 'code': 'ro', 'small': False, 'regional': False, 'flag': '🇷🇴'},
|
| 40 |
+
{'name': 'Slovak', 'code': 'sk', 'small': True, 'regional': False, 'flag': '🇸🇰'},
|
| 41 |
+
{'name': 'Slovenian', 'code': 'sl', 'small': True, 'regional': False, 'flag': '🇸🇮'},
|
| 42 |
+
{'name': 'Spanish', 'code': 'es', 'small': False, 'regional': False, 'flag': '🇪🇸'},
|
| 43 |
+
{'name': 'Swedish', 'code': 'sv', 'small': True, 'regional': False, 'flag': '🇸🇪'},
|
| 44 |
+
{'name': 'Breton', 'code': 'br', 'small': True, 'regional': True, 'flag': '🏴'},
|
| 45 |
+
{'name': 'Welsh', 'code': 'cy', 'small': True, 'regional': True, 'flag': '🏴'},
|
| 46 |
+
{'name': 'Scottish Gaelic', 'code': 'gd', 'small': True, 'regional': True, 'flag': '🏴'},
|
| 47 |
+
{'name': 'Ukrainian', 'code': 'uk', 'small': False, 'regional': False, 'flag': '🇺🇦'}
|
| 48 |
+
]
|
| 49 |
+
|
| 50 |
class WordAnalysis(BaseModel):
|
| 51 |
form: str = Field(description="The word as it appears in the text")
|
| 52 |
grammar_comments: str = Field(description="Grammatical comments about the word")
|
|
|
|
| 103 |
)
|
| 104 |
chat_model_util = ChatHuggingFace(llm=llm_util)
|
| 105 |
|
| 106 |
+
# Modelli per lingue meno diffuse: EuroLLM per EU ufficiali, Aya per regionali
|
| 107 |
+
EUROLLM_MODEL = "utter-project/EuroLLM-22B-Instruct-2512"
|
| 108 |
+
AYA_MODEL = "CohereLabs/aya-expanse-32b"
|
| 109 |
+
HF_ROUTER_URL = "https://router.huggingface.co/v1/chat/completions"
|
| 110 |
+
|
| 111 |
# Dizionario globale per gestire i timer dei messaggi (per chat_id)
|
| 112 |
active_timers = {}
|
| 113 |
|
|
|
|
| 305 |
)
|
| 306 |
return redirect('chat_detail', chat_id=chat.id)
|
| 307 |
|
| 308 |
+
return render(request, 'web/new_chat.html', {
|
| 309 |
+
'profile': profile,
|
| 310 |
+
'supported_languages': SUPPORTED_LANGUAGES
|
| 311 |
+
})
|
| 312 |
|
| 313 |
|
| 314 |
@csrf_exempt
|
|
|
|
| 336 |
|
| 337 |
|
| 338 |
return render(request, 'web/chat_settings.html', {
|
| 339 |
+
'chat': chat,
|
| 340 |
+
'supported_languages': SUPPORTED_LANGUAGES
|
| 341 |
})
|
| 342 |
|
| 343 |
|
|
|
|
| 366 |
mcp_list = request.POST.getlist('mcp_tools')
|
| 367 |
profile.global_mcp_tools = [url.strip() for url in mcp_list if url.strip()]
|
| 368 |
profile.global_timer = int(request.POST.get('global_timer', profile.global_timer))
|
| 369 |
+
profile.global_language = request.POST.get('global_language', profile.global_language)
|
| 370 |
profile.save()
|
| 371 |
return redirect('index')
|
| 372 |
|
| 373 |
return render(request, 'web/settings.html', {
|
| 374 |
+
'profile': profile,
|
| 375 |
+
'supported_languages': SUPPORTED_LANGUAGES
|
| 376 |
})
|
| 377 |
|
| 378 |
|
|
|
|
| 405 |
chat = get_object_or_404(ChatSession, id=chat_id, user=user)
|
| 406 |
|
| 407 |
target_lang = chat.language
|
| 408 |
+
lang_info = next((l for l in SUPPORTED_LANGUAGES if l['name'] == target_lang), None)
|
| 409 |
+
# Per le lingue "piccole" usiamo MADLAD in cascata o per traduzioni dirette
|
| 410 |
+
is_small_lang = lang_info['small'] if lang_info else False
|
| 411 |
|
| 412 |
+
# Inizialmente usiamo sempre il modello base per la struttura
|
| 413 |
+
current_util_model = chat_model_util
|
| 414 |
+
|
| 415 |
# Prompt logic based on bot copy.py
|
| 416 |
if action == "grammar_check":
|
| 417 |
prompt = (
|
|
|
|
| 434 |
elif action == "translate_to_target":
|
| 435 |
prompt = f"Translate the following English text into {target_lang}. Provide ONLY the translation without any introduction.\n\nText: {text_to_analyze}."
|
| 436 |
elif action == "word_to_word":
|
| 437 |
+
if target_lang:
|
| 438 |
+
lang_code = lang_info['code'] if lang_info else "en"
|
| 439 |
+
api_url = f"https://randusertry-stanzalazymodels.hf.space/{lang_code}/analyze"
|
| 440 |
+
try:
|
| 441 |
+
# Chiamata API esterna (Stanza as a Service) per analisi morfo-sintattica
|
| 442 |
+
resp = requests.post(api_url, json={"text": text_to_analyze}, timeout=60)
|
| 443 |
+
if resp.status_code == 200:
|
| 444 |
+
data = resp.json()
|
| 445 |
+
word_objects = []
|
| 446 |
+
lemmas_to_translate = []
|
| 447 |
+
|
| 448 |
+
# Gestione flessibile della risposta (flat list vs nested sentences)
|
| 449 |
+
if isinstance(data, list):
|
| 450 |
+
# Caso 1: Lista piatta di parole (come visto nell'output PowerShell dell'utente)
|
| 451 |
+
if len(data) > 0 and isinstance(data[0], dict) and ('text' in data[0] or 'lemma' in data[0]):
|
| 452 |
+
words_to_process = data
|
| 453 |
+
else:
|
| 454 |
+
# Caso 2: Lista di frasi (che a loro volta sono liste di parole)
|
| 455 |
+
words_to_process = []
|
| 456 |
+
for sent in data:
|
| 457 |
+
if isinstance(sent, list):
|
| 458 |
+
words_to_process.extend(sent)
|
| 459 |
+
elif isinstance(sent, dict) and "words" in sent:
|
| 460 |
+
words_to_process.extend(sent["words"])
|
| 461 |
+
else:
|
| 462 |
+
# Caso 3: Dizionario con chiave "sentences"
|
| 463 |
+
words_to_process = []
|
| 464 |
+
for sent in data.get("sentences", []):
|
| 465 |
+
words_to_process.extend(sent if isinstance(sent, list) else sent.get("words", []))
|
| 466 |
+
|
| 467 |
+
for word in words_to_process:
|
| 468 |
+
if not isinstance(word, dict): continue
|
| 469 |
+
# L'API usa 'pos' e 'morph' invece di 'upos' e 'feats'
|
| 470 |
+
upos = word.get("pos") or word.get("upos", "")
|
| 471 |
+
morph = word.get("morph") or word.get("feats", "")
|
| 472 |
+
|
| 473 |
+
if upos == 'PUNCT': continue
|
| 474 |
+
|
| 475 |
+
word_objects.append({
|
| 476 |
+
"form": word.get("text"),
|
| 477 |
+
"grammar_comments": f"{upos} {morph}".strip(),
|
| 478 |
+
"lemma": word.get("lemma"),
|
| 479 |
+
"translation": ""
|
| 480 |
+
})
|
| 481 |
+
if word.get("lemma"):
|
| 482 |
+
lemmas_to_translate.append(word.get("lemma"))
|
| 483 |
+
|
| 484 |
+
# Route to EuroLLM (official EU) or Aya (regional) for translation
|
| 485 |
+
if lemmas_to_translate:
|
| 486 |
+
lemmas_to_translate = [l for l in lemmas_to_translate if any(c.isalpha() for c in l)]
|
| 487 |
+
lang_meta = next((l for l in SUPPORTED_LANGUAGES if l['name'] == chat.language), None)
|
| 488 |
+
is_regional = lang_meta.get('regional', False) if lang_meta else False
|
| 489 |
+
model_id = AYA_MODEL if is_regional else EUROLLM_MODEL
|
| 490 |
+
active_source = f"Stanza + {'Aya 32B' if is_regional else 'EuroLLM 22B'}"
|
| 491 |
+
|
| 492 |
+
word_count = len(lemmas_to_translate)
|
| 493 |
+
# Number each lemma so the model knows exactly how many to return
|
| 494 |
+
numbered_input = "\n".join(f"{i+1}. {l}" for i, l in enumerate(lemmas_to_translate))
|
| 495 |
+
hf_headers = {"Authorization": f"Bearer {settings.HF_TOKEN}"}
|
| 496 |
+
sys_msg = (
|
| 497 |
+
f"You are a linguistic expert in {chat.language}. "
|
| 498 |
+
f"You will receive exactly {word_count} numbered {chat.language} words/lemmas. "
|
| 499 |
+
f"Translate each one into English. "
|
| 500 |
+
f"Reply with exactly {word_count} numbered lines in the format: '1. translation'. "
|
| 501 |
+
f"Do NOT add any extra text, commentary, or blank lines."
|
| 502 |
+
)
|
| 503 |
+
try:
|
| 504 |
+
print(f"DEBUG - Routing to {model_id} ({word_count} words)")
|
| 505 |
+
r = requests.post(HF_ROUTER_URL, headers=hf_headers, json={
|
| 506 |
+
"model": model_id,
|
| 507 |
+
"messages": [{"role": "system", "content": sys_msg},
|
| 508 |
+
{"role": "user", "content": numbered_input}],
|
| 509 |
+
"max_tokens": 800, "temperature": 0.1
|
| 510 |
+
}, timeout=120)
|
| 511 |
+
if r.status_code == 200:
|
| 512 |
+
translated_bulk = r.json()['choices'][0]['message']['content'].strip()
|
| 513 |
+
print(f"DEBUG - Translated: {translated_bulk[:120]}...")
|
| 514 |
+
# Strip numbering: "1. be" → "be"
|
| 515 |
+
import re
|
| 516 |
+
translations = [re.sub(r'^\d+\.\s*', '', t).strip() for t in translated_bulk.split("\n") if t.strip()]
|
| 517 |
+
if len(translations) == len(lemmas_to_translate):
|
| 518 |
+
for i, trans in enumerate(translations):
|
| 519 |
+
if i < len(word_objects):
|
| 520 |
+
word_objects[i]["translation"] = trans
|
| 521 |
+
else:
|
| 522 |
+
print(f"DEBUG - Mismatch ({len(translations)} vs {len(lemmas_to_translate)}), sequential fallback.")
|
| 523 |
+
for i, lemma in enumerate(lemmas_to_translate):
|
| 524 |
+
if i >= len(word_objects): break
|
| 525 |
+
r2 = requests.post(HF_ROUTER_URL, headers=hf_headers, json={
|
| 526 |
+
"model": model_id,
|
| 527 |
+
"messages": [{"role": "system", "content": f"Translate this {chat.language} word to English. Reply with ONLY the English word."},
|
| 528 |
+
{"role": "user", "content": lemma}],
|
| 529 |
+
"max_tokens": 50, "temperature": 0.1
|
| 530 |
+
}, timeout=45)
|
| 531 |
+
if r2.status_code == 200:
|
| 532 |
+
word_objects[i]["translation"] = r2.json()['choices'][0]['message']['content'].strip()
|
| 533 |
+
else:
|
| 534 |
+
print(f"DEBUG - {model_id} error {r.status_code}: {r.text}")
|
| 535 |
+
raise Exception(f"API Error {r.status_code}")
|
| 536 |
+
except Exception as e:
|
| 537 |
+
print(f"Translation failure ({model_id}): {repr(e)}")
|
| 538 |
+
for obj in word_objects:
|
| 539 |
+
if not obj["translation"]:
|
| 540 |
+
obj["translation"] = "[error]"
|
| 541 |
+
else:
|
| 542 |
+
active_source = 'Stanza API'
|
| 543 |
+
|
| 544 |
+
return JsonResponse({
|
| 545 |
+
'result': {"words": word_objects},
|
| 546 |
+
'is_structured': True,
|
| 547 |
+
'source': active_source
|
| 548 |
+
})
|
| 549 |
+
except Exception as e:
|
| 550 |
+
print(f"Stanza API error: {e}")
|
| 551 |
+
|
| 552 |
+
# Fallback per Breton o se l'API Stanza fallisce
|
| 553 |
prompt = (
|
| 554 |
f"Provide a word-for-word literal translation of the following text into English (or {target_lang} if it is already in English). "
|
| 555 |
+
f"Format the output as a JSON with 'words' as key containing a list of objects, each with 'form', 'lemma', 'grammar_comments', and 'translation'. "
|
|
|
|
| 556 |
f"\n\nText: {text_to_analyze}"
|
| 557 |
)
|
| 558 |
elif action == "grammatical_explanation":
|
| 559 |
prompt = (
|
| 560 |
f"Provide a stylistic analysis of the following text in {target_lang}. "
|
| 561 |
+
f"For each sentence, explain the stylistic choices, why it was said in that way, and identify if it uses colloquialisms, slang, or dialect. Also make sure to mention noteworthy grammatical structures and idiomatic expressions."
|
| 562 |
f"\n\nText: {text_to_analyze}"
|
| 563 |
)
|
| 564 |
else:
|
|
|
|
| 573 |
SystemMessage(content="You are a highly capable linguistic assistant that always responds in valid JSON format as requested."),
|
| 574 |
HumanMessage(content=full_prompt)
|
| 575 |
]
|
| 576 |
+
response_ai = current_util_model.invoke(messages)
|
| 577 |
try:
|
| 578 |
ai_text = parser.parse(response_ai.content)
|
| 579 |
is_structured = True
|
|
|
|
| 586 |
SystemMessage(content="You are a highly capable linguistic assistant that always responds in valid JSON format as requested."),
|
| 587 |
HumanMessage(content=full_prompt)
|
| 588 |
]
|
| 589 |
+
response_ai = current_util_model.invoke(messages)
|
| 590 |
try:
|
| 591 |
ai_text = parser.parse(response_ai.content)
|
| 592 |
is_structured = True
|
|
|
|
| 599 |
SystemMessage(content="You are a linguistic expert assistant. Always respond in JSON format."),
|
| 600 |
HumanMessage(content=full_prompt)
|
| 601 |
]
|
| 602 |
+
response_ai = current_util_model.invoke(messages)
|
| 603 |
try:
|
| 604 |
ai_text = parser.parse(response_ai.content)
|
| 605 |
is_structured = True
|
|
|
|
| 612 |
SystemMessage(content="You are a native speaker linguistic assistant. Always respond in JSON format."),
|
| 613 |
HumanMessage(content=full_prompt)
|
| 614 |
]
|
| 615 |
+
response_ai = current_util_model.invoke(messages)
|
| 616 |
try:
|
| 617 |
ai_text = parser.parse(response_ai.content)
|
| 618 |
is_structured = True
|
| 619 |
except Exception:
|
| 620 |
ai_text = response_ai.content
|
| 621 |
+
elif action in ["translate", "translate_to_target"]:
|
| 622 |
+
# Per le traduzioni in lingue piccole, usiamo direttamente MADLAD (Base LLM)
|
| 623 |
+
if is_small_lang:
|
| 624 |
+
target_code = "en" if action == "translate" else lang_info['code']
|
| 625 |
+
madlad_prompt = f"<2{target_code}> {text_to_analyze}"
|
| 626 |
+
try:
|
| 627 |
+
ai_text = llm_small_lang.invoke(madlad_prompt).strip()
|
| 628 |
+
except Exception as e:
|
| 629 |
+
print(f"MADLAD Translate error: {e}")
|
| 630 |
+
# Fallback a modello base
|
| 631 |
+
messages = [
|
| 632 |
+
SystemMessage(content="You are a high-quality translator. Provide only the translation without any introduction."),
|
| 633 |
+
HumanMessage(content=prompt)
|
| 634 |
+
]
|
| 635 |
+
response_ai = chat_model_util.invoke(messages)
|
| 636 |
+
ai_text = response_ai.content
|
| 637 |
+
else:
|
| 638 |
+
messages = [
|
| 639 |
+
SystemMessage(content="You are a high-quality translator. Provide only the translation without any introduction."),
|
| 640 |
+
HumanMessage(content=prompt)
|
| 641 |
+
]
|
| 642 |
+
response_ai = chat_model_util.invoke(messages)
|
| 643 |
+
ai_text = response_ai.content
|
| 644 |
else:
|
| 645 |
messages = [
|
| 646 |
SystemMessage(content="You are a highly capable linguistic assistant."),
|
| 647 |
HumanMessage(content=prompt)
|
| 648 |
]
|
| 649 |
+
response_ai = current_util_model.invoke(messages)
|
| 650 |
ai_text = response_ai.content
|
| 651 |
|
| 652 |
+
# --- POST-PROCESSING per Lingue Piccole (MADLAD Overlay) ---
|
| 653 |
+
if is_small_lang and is_structured:
|
| 654 |
+
lang_code = lang_info['code'] if lang_info else "en"
|
| 655 |
+
if action == "grammar_check" and isinstance(ai_text, dict) and "sentences" in ai_text:
|
| 656 |
+
for s in ai_text["sentences"]:
|
| 657 |
+
if s.get("rewritten"):
|
| 658 |
+
try:
|
| 659 |
+
# Prompt con tag MADLAD: <2xx> per output nella lingua target
|
| 660 |
+
refine_prompt = f"<2{lang_code}> {s['rewritten']}"
|
| 661 |
+
refined_text = llm_small_lang.invoke(refine_prompt)
|
| 662 |
+
s["rewritten"] = refined_text.strip()
|
| 663 |
+
except Exception as e:
|
| 664 |
+
print(f"Refinement error: {e}")
|
| 665 |
+
|
| 666 |
+
elif action == "native_rewrite" and isinstance(ai_text, dict) and "suggestions" in ai_text:
|
| 667 |
+
for s in ai_text["suggestions"]:
|
| 668 |
+
if s.get("text"):
|
| 669 |
+
try:
|
| 670 |
+
refine_prompt = f"<2{lang_code}> {s['text']}"
|
| 671 |
+
refined_text = llm_small_lang.invoke(refine_prompt)
|
| 672 |
+
s["text"] = refined_text.strip()
|
| 673 |
+
except Exception as e:
|
| 674 |
+
print(f"Refinement error: {e}")
|
| 675 |
+
|
| 676 |
+
return JsonResponse({
|
| 677 |
+
'result': ai_text,
|
| 678 |
+
'is_structured': is_structured,
|
| 679 |
+
'source': 'AI Model (Llama/Qwen)' if action == 'word_to_word' else None
|
| 680 |
+
})
|
| 681 |
except Exception as e:
|
| 682 |
return JsonResponse({'error': str(e)}, status=500)
|
| 683 |
|
|
|
|
| 743 |
except Exception as e:
|
| 744 |
print(f"Error AI: {e}")
|
| 745 |
PendingMessage.objects.create(chat=chat, content="Sorry, I'm having trouble thinking right now.", is_user=False)
|
| 746 |
+
@csrf_exempt
|
| 747 |
+
def tts_proxy(request):
|
| 748 |
+
if request.method == "POST":
|
| 749 |
+
try:
|
| 750 |
+
data = json.loads(request.body)
|
| 751 |
+
text = data.get("text", "")
|
| 752 |
+
lang_name = data.get("lang", "English")
|
| 753 |
+
|
| 754 |
+
# Look up lang code from SUPPORTED_LANGUAGES
|
| 755 |
+
lang_code = "en"
|
| 756 |
+
for l in SUPPORTED_LANGUAGES:
|
| 757 |
+
if l['name'] == lang_name:
|
| 758 |
+
lang_code = l['code']
|
| 759 |
+
break
|
| 760 |
+
|
| 761 |
+
url = "https://feliksius-ai-translation.hf.space/v1/tts"
|
| 762 |
+
payload = {
|
| 763 |
+
"input_text": text,
|
| 764 |
+
"from_language": lang_code
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
response = requests.post(url, json=payload, timeout=20)
|
| 768 |
+
if response.status_code == 200:
|
| 769 |
+
# Return the audio as a wav response
|
| 770 |
+
django_response = HttpResponse(response.content, content_type="audio/wav")
|
| 771 |
+
django_response['Content-Disposition'] = 'inline; filename="speech.wav"'
|
| 772 |
+
return django_response
|
| 773 |
+
else:
|
| 774 |
+
return JsonResponse({"error": f"TTS microservice error: {response.status_code}"}, status=response.status_code)
|
| 775 |
+
except Exception as e:
|
| 776 |
+
return JsonResponse({"error": str(e)}, status=500)
|
| 777 |
+
return HttpResponse(status=405)
|