randusertry commited on
Commit
27dfca2
·
verified ·
1 Parent(s): bc77bfc

Upload 21 files

Browse files
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 = '<table><thead><tr><th>Form</th><th>Lemma</th><th>Translation</th></tr></thead><tbody>';
 
 
 
 
129
  data.result.words.forEach(row => {
130
  tableHtml += `<tr>
131
- <td>${row.form || ''}</td>
132
- <td>${row.lemma || ''}</td>
133
- <td>${row.translation || ''}</td>
 
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
- <label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.9rem;">🌐 Target Language</label>
21
- <input type="text" name="lang" value="{{ chat.language }}" placeholder="e.g. English, Italian..." required>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <input type="text" name="lang" value="{{ profile.global_language }}" placeholder="e.g. Italian, Japanese, Elvish..." required>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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', {'profile': profile})
 
 
 
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 Markdown table with exactly four columns: 'Form', 'Grammar Comments', 'Lemma', and 'Translation'. "
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 = chat_model_util.invoke(messages)
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 = chat_model_util.invoke(messages)
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 = chat_model_util.invoke(messages)
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 = chat_model_util.invoke(messages)
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 = chat_model_util.invoke(messages)
464
  ai_text = response_ai.content
465
 
466
- return JsonResponse({'result': ai_text, 'is_structured': is_structured})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)