Initial commit: Ollama Translator Web App
This commit is contained in:
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Ambiente virtuale
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# File Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# File di log
|
||||
*.log
|
||||
|
||||
231
ReadMe.MD
Normal file
231
ReadMe.MD
Normal file
@@ -0,0 +1,231 @@
|
||||
# 🌍 Traduttore Ollama
|
||||
|
||||
Un'applicazione web moderna per tradurre testi lunghi utilizzando modelli locali di Ollama. Supporta oltre 30 lingue e utilizza chunking intelligente per gestire testi di qualsiasi lunghezza.
|
||||
|
||||
## 📋 Caratteristiche
|
||||
|
||||
- ✅ **Supporto multi-lingua**: 30+ lingue (cinese, inglese, spagnolo, francese, tedesco, italiano, giapponese, coreano, russo, arabo, e molte altre)
|
||||
- ✅ **Gestione testi lunghi**: Chunking automatico a 500 token per elaborare documenti estesi
|
||||
- ✅ **UI moderna e responsiva**: Design elegante con gradient background e animazioni fluide
|
||||
- ✅ **Barra di progresso**: Visualizzazione in tempo reale dell'elaborazione dei chunk
|
||||
- ✅ **Ottimizzazione modello**: Parametri `top_p=0.6` e `repeat_penalty=1.1` per miglior qualità
|
||||
- ✅ **Copia negli appunti**: Pulsante per copiare la traduzione con un click
|
||||
- ✅ **Gestione errori robusta**: Messaggi di errore chiari e dettagliati
|
||||
- ✅ **Health check**: Verificare lo stato di Ollama e i modelli disponibili
|
||||
|
||||
## 🚀 Installazione Rapida
|
||||
|
||||
### Prerequisiti
|
||||
|
||||
- **Python 3.8+**
|
||||
- **Ollama** in esecuzione su `192.168.0.170:11434` con il modello `xieweicong95/HY-MT1.5-1.8B` scaricato
|
||||
|
||||
### Step 1: Clona o crea la directory del progetto
|
||||
|
||||
mkdir ollama-traduttore
|
||||
cd ollama-traduttore
|
||||
|
||||
### Step 2: Crea l'ambiente virtuale
|
||||
|
||||
**Su Windows (CMD o PowerShell):**
|
||||
python -m venv venv
|
||||
venv\Scripts\activate
|
||||
|
||||
**Su Linux/Mac:**
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
### Step 3: Installa le dipendenze
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
### Step 4: Assicurati che Ollama sia pronto
|
||||
|
||||
# Verifica che Ollama sia in esecuzione
|
||||
ollama list
|
||||
|
||||
# Se il modello non è presente, scaricalo
|
||||
ollama pull xieweicong95/HY-MT1.5-1.8B
|
||||
|
||||
# Avvia Ollama (se non è già running)
|
||||
ollama serve
|
||||
|
||||
### Step 5: Avvia l'applicazione
|
||||
|
||||
python app.py
|
||||
|
||||
L'applicazione sarà disponibile su: **`http://localhost:5000`**
|
||||
|
||||
## 📁 Struttura del Progetto
|
||||
|
||||
ollama-traduttore/
|
||||
├── app.py # Backend Flask
|
||||
├── requirements.txt # Dipendenze Python
|
||||
├── templates/
|
||||
│ └── index.html # Frontend HTML/CSS/JS
|
||||
└── venv/ # Ambiente virtuale
|
||||
|
||||
## 🔧 Configurazione
|
||||
|
||||
### Cambiare l'indirizzo di Ollama
|
||||
|
||||
Se Ollama è su un indirizzo/porta diverso, modifica `app.py`:
|
||||
|
||||
OLLAMA_BASE_URL = "http://192.168.0.170:11434" # Cambia questo
|
||||
|
||||
### Cambiare il modello
|
||||
|
||||
Per usare un modello diverso, modifica in `app.py`:
|
||||
|
||||
OLLAMA_MODEL = "xieweicong95/HY-MT1.5-1.8B" # Cambia questo
|
||||
|
||||
### Regolare i parametri di traduzione
|
||||
|
||||
I parametri di qualità sono configurabili in `app.py`:
|
||||
|
||||
OLLAMA_PARAMS = {
|
||||
"top_p": 0.6, # Diversità del sampling (0.0-1.0)
|
||||
"repeat_penalty": 1.1 # Penalità per ripetizioni (1.0-2.0)
|
||||
}
|
||||
|
||||
## 📖 Come Usare
|
||||
|
||||
1. **Incolla il testo** da tradurre nella textarea
|
||||
2. **Seleziona la lingua di destinazione** dal dropdown
|
||||
3. **Opzionalmente**, specifica la lingua sorgente (default: autodetect)
|
||||
4. **Clicca "Traduci"** e attendi il completamento
|
||||
5. **Copia la traduzione** con il pulsante "Copia" o selezionala manualmente
|
||||
|
||||
### Lingue Supportate
|
||||
|
||||
#### Principali / Molto comuni
|
||||
- Cinese (mandarino) semplificato
|
||||
- Cinese tradizionale
|
||||
- Cantonese (dialetto)
|
||||
- Inglese
|
||||
- Spagnolo
|
||||
- Francese
|
||||
- Tedesco
|
||||
- Italiano
|
||||
- Portoghese
|
||||
- Giapponese
|
||||
- Coreano
|
||||
- Russo
|
||||
- Arabo
|
||||
- Turco
|
||||
|
||||
#### Europee e minori
|
||||
- Olandese
|
||||
- Polacco
|
||||
- Ceco
|
||||
- Islandese
|
||||
- Estone
|
||||
- Ucraino
|
||||
|
||||
#### Asia e Medio Oriente
|
||||
- Vietnamita
|
||||
- Tailandese
|
||||
- Malese
|
||||
- Indonesiano
|
||||
- Filippino / Tagalog
|
||||
- Birmano (Myanmar)
|
||||
- Khmer / Cambogiano
|
||||
- Persiano
|
||||
- Ebraico
|
||||
- Hindi
|
||||
- Marathi
|
||||
- Bengali
|
||||
- Tamil
|
||||
- Gujarati
|
||||
|
||||
## 🔌 API Endpoints
|
||||
|
||||
### POST `/api/translate`
|
||||
|
||||
Traduce un testo usando Ollama.
|
||||
|
||||
**Request:**
|
||||
{
|
||||
"prompt": "Translate the following text into [Language]...",
|
||||
"model": "xieweicong95/HY-MT1.5-1.8B",
|
||||
"stream": false
|
||||
}
|
||||
|
||||
**Response:**
|
||||
{
|
||||
"success": true,
|
||||
"translation": "Testo tradotto...",
|
||||
"response": "Testo tradotto...",
|
||||
"model": "xieweicong95/HY-MT1.5-1.8B"
|
||||
}
|
||||
|
||||
### GET `/api/health`
|
||||
|
||||
Verifica lo stato di Ollama e i modelli disponibili.
|
||||
|
||||
**Response:**
|
||||
{
|
||||
"status": "healthy",
|
||||
"ollama_url": "http://192.168.0.170:11434",
|
||||
"available_models": ["xieweicong95/HY-MT1.5-1.8B", "..."],
|
||||
"target_model": "xieweicong95/HY-MT1.5-1.8B",
|
||||
"model_available": true
|
||||
}
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Errore: "Impossibile connettersi a Ollama"
|
||||
|
||||
1. Verifica che Ollama sia in esecuzione: `ollama serve`
|
||||
2. Controlla l'indirizzo IP e la porta in `app.py`
|
||||
3. Assicurati che il firewall non blocchi la connessione
|
||||
|
||||
### Errore: "Modello non trovato"
|
||||
|
||||
Scarica il modello:
|
||||
ollama pull xieweicong95/HY-MT1.5-1.8B
|
||||
|
||||
### Timeout della richiesta
|
||||
|
||||
Aumenta il timeout in `app.py`:
|
||||
timeout=300 # Aumenta da 300 secondi (5 minuti)
|
||||
|
||||
### Traduzione lenta
|
||||
|
||||
- Riduci il numero di chunk aumentando `maxTokens` in `index.html`
|
||||
- Aumenta le risorse assegnate a Ollama
|
||||
- Considera l'uso di un modello più piccolo
|
||||
|
||||
## 📊 Performance
|
||||
|
||||
- **Tempo di risposta**: Dipende dal modello e dalla lunghezza del testo
|
||||
- **Limite chunk**: 500 token (configurabile)
|
||||
- **Timeout**: 5 minuti per chunk
|
||||
- **RAM consigliata**: Almeno 8GB per il modello HY-MT1.5-1.8B
|
||||
|
||||
## 🔐 Sicurezza
|
||||
|
||||
- L'applicazione è accessibile solo in locale per impostazione predefinita
|
||||
- I dati non vengono salvati, solo elaborati in tempo reale
|
||||
- Usa HTTPS in produzione
|
||||
|
||||
## 📝 Licenza
|
||||
|
||||
Questo progetto utilizza Ollama (Open Source) e Flask (BSD License).
|
||||
|
||||
## 🤝 Contributi
|
||||
|
||||
Suggerimenti e miglioramenti sono benvenuti!
|
||||
|
||||
## 📧 Supporto
|
||||
|
||||
Per problemi o domande:
|
||||
1. Verifica il section Troubleshooting
|
||||
2. Controlla i log di Flask per errori
|
||||
3. Assicurati che Ollama sia configurato correttamente
|
||||
|
||||
---
|
||||
|
||||
**Versione**: 1.0.0
|
||||
**Ultima modifica**: Gennaio 2026
|
||||
**Modello**: xieweicong95/HY-MT1.5-1.8B
|
||||
146
app.py
Normal file
146
app.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Configurazione Ollama
|
||||
OLLAMA_BASE_URL = "http://192.168.0.170:11434"
|
||||
OLLAMA_MODEL = "xieweicong95/HY-MT1.5-1.8B"
|
||||
OLLAMA_PARAMS = {
|
||||
"top_p": 0.6,
|
||||
"repeat_penalty": 1.1
|
||||
}
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/api/translate', methods=['POST'])
|
||||
def translate():
|
||||
"""
|
||||
Endpoint per tradurre testo usando Ollama.
|
||||
Accetta JSON con:
|
||||
- prompt: il prompt da inviare a Ollama
|
||||
- model: il modello da usare
|
||||
- stream: boolean per streaming (default: False)
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'prompt' not in data:
|
||||
return jsonify({'error': 'Prompt non fornito'}), 400
|
||||
|
||||
prompt = data.get('prompt')
|
||||
model = data.get('model', OLLAMA_MODEL)
|
||||
stream = data.get('stream', False)
|
||||
|
||||
# Costruisci il payload per Ollama
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": stream,
|
||||
**OLLAMA_PARAMS # Aggiungi i parametri top_p e repeat_penalty
|
||||
}
|
||||
|
||||
# Invia la richiesta a Ollama
|
||||
response = requests.post(
|
||||
f"{OLLAMA_BASE_URL}/api/generate",
|
||||
json=payload,
|
||||
timeout=300 # Timeout di 5 minuti per testi lunghi
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return jsonify({
|
||||
'error': f'Errore Ollama: {response.status_code}',
|
||||
'details': response.text
|
||||
}), response.status_code
|
||||
|
||||
# Se non è streaming, la risposta è JSON diretto
|
||||
if not stream:
|
||||
result = response.json()
|
||||
translation = result.get('response', '').strip()
|
||||
|
||||
# Pulisci la traduzione da eventuali prefissi
|
||||
if translation.startswith('Translation:'):
|
||||
translation = translation.replace('Translation:', '', 1).strip()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'translation': translation,
|
||||
'response': translation,
|
||||
'model': model
|
||||
})
|
||||
else:
|
||||
# Se è streaming, concatena le risposte
|
||||
full_response = ""
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
chunk = json.loads(line)
|
||||
full_response += chunk.get('response', '')
|
||||
|
||||
translation = full_response.strip()
|
||||
if translation.startswith('Translation:'):
|
||||
translation = translation.replace('Translation:', '', 1).strip()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'translation': translation,
|
||||
'response': translation,
|
||||
'model': model
|
||||
})
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return jsonify({
|
||||
'error': 'Timeout della richiesta',
|
||||
'details': 'La richiesta a Ollama ha impiegato troppo tempo'
|
||||
}), 408
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
return jsonify({
|
||||
'error': 'Errore di connessione',
|
||||
'details': f'Impossibile connettersi a Ollama su {OLLAMA_BASE_URL}'
|
||||
}), 503
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'error': 'Errore interno del server',
|
||||
'details': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/health', methods=['GET'])
|
||||
def health():
|
||||
"""Controlla lo stato di Ollama"""
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{OLLAMA_BASE_URL}/api/tags",
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
models = response.json().get('models', [])
|
||||
model_names = [m.get('name') for m in models]
|
||||
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'ollama_url': OLLAMA_BASE_URL,
|
||||
'available_models': model_names,
|
||||
'target_model': OLLAMA_MODEL,
|
||||
'model_available': OLLAMA_MODEL in model_names
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'unhealthy',
|
||||
'error': f'Ollama ha risposto con status {response.status_code}'
|
||||
}), 503
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'error': str(e),
|
||||
'ollama_url': OLLAMA_BASE_URL
|
||||
}), 503
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Flask==3.0.0
|
||||
requests==2.31.0
|
||||
587
templates/index.html
Normal file
587
templates/index.html
Normal file
@@ -0,0 +1,587 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Traduttore con Ollama</title>
|
||||
<style>
|
||||
:root {
|
||||
--color-white: rgba(255, 255, 255, 1);
|
||||
--color-black: rgba(0, 0, 0, 1);
|
||||
--color-cream-50: rgba(252, 252, 249, 1);
|
||||
--color-gray-300: rgba(167, 169, 169, 1);
|
||||
--color-slate-900: rgba(19, 52, 59, 1);
|
||||
--color-teal-500: rgba(33, 128, 141, 1);
|
||||
--color-teal-600: rgba(29, 116, 128, 1);
|
||||
--color-teal-300: rgba(50, 184, 198, 1);
|
||||
--color-background: var(--color-cream-50);
|
||||
--color-surface: var(--color-white);
|
||||
--color-text: var(--color-slate-900);
|
||||
--color-text-secondary: var(--color-gray-300);
|
||||
--color-primary: var(--color-teal-500);
|
||||
--color-primary-hover: var(--color-teal-600);
|
||||
--color-focus-ring: rgba(33, 128, 141, 0.4);
|
||||
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--space-16: 16px;
|
||||
--space-20: 20px;
|
||||
--space-24: 24px;
|
||||
--space-32: 32px;
|
||||
--radius-base: 8px;
|
||||
--radius-lg: 12px;
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.04), 0 2px 4px -1px rgba(0, 0, 0, 0.02);
|
||||
--duration-normal: 250ms;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #e8f4f8 100%);
|
||||
color: var(--color-text);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-20);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: var(--space-32);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-32);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: var(--color-slate-900);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: var(--space-24);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: var(--radius-base);
|
||||
resize: vertical;
|
||||
transition: border-color var(--duration-normal);
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px var(--color-focus-ring);
|
||||
}
|
||||
|
||||
.char-count {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-20);
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: var(--radius-base);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px;
|
||||
padding-right: 40px;
|
||||
transition: border-color var(--duration-normal);
|
||||
}
|
||||
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px var(--color-focus-ring);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: var(--radius-base);
|
||||
cursor: pointer;
|
||||
transition: all var(--duration-normal);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
flex: 1;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(33, 128, 141, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f0f0f0;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.result-container {
|
||||
margin-top: var(--space-32);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.result-container.show {
|
||||
display: block;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.result-box {
|
||||
background: #f9f9f9;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: var(--radius-base);
|
||||
padding: var(--space-20);
|
||||
margin-bottom: var(--space-16);
|
||||
}
|
||||
|
||||
.result-box h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.result-box p {
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
color: var(--color-text);
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-top: var(--space-20);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-container.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--color-primary);
|
||||
transition: width 0.3s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.status-message.error {
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.status-message.success {
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
border: none;
|
||||
border-radius: var(--radius-base);
|
||||
cursor: pointer;
|
||||
margin-top: 12px;
|
||||
transition: all var(--duration-normal);
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.copy-btn.copied {
|
||||
background: #388e3c;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: var(--space-20);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🌍 Traduttore Ollama</h1>
|
||||
<p>Traduci testi lunghi con modelli locali</p>
|
||||
</div>
|
||||
|
||||
<form id="translatorForm">
|
||||
<div class="form-group">
|
||||
<label for="sourceText">Testo da tradurre</label>
|
||||
<textarea id="sourceText" placeholder="Inserisci il testo che desideri tradurre (anche molto lungo)..." required></textarea>
|
||||
<div class="char-count">
|
||||
<span id="charCount">0</span> caratteri
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="targetLanguage">Lingua di destinazione</label>
|
||||
<select id="targetLanguage" required>
|
||||
<option value="">Seleziona una lingua</option>
|
||||
<optgroup label="Principali / Molto comuni">
|
||||
<option value="Chinese (Simplified)">Cinese (mandarino) semplificato</option>
|
||||
<option value="Chinese (Traditional)">Cinese tradizionale</option>
|
||||
<option value="Cantonese">Cantonese (dialetto)</option>
|
||||
<option value="English">Inglese</option>
|
||||
<option value="Spanish">Spagnolo</option>
|
||||
<option value="French">Francese</option>
|
||||
<option value="German">Tedesco</option>
|
||||
<option value="Italian">Italiano</option>
|
||||
<option value="Portuguese">Portoghese</option>
|
||||
<option value="Japanese">Giapponese</option>
|
||||
<option value="Korean">Coreano</option>
|
||||
<option value="Russian">Russo</option>
|
||||
<option value="Arabic">Arabo</option>
|
||||
<option value="Turkish">Turco</option>
|
||||
</optgroup>
|
||||
<optgroup label="Europee e minori">
|
||||
<option value="Dutch">Olandese</option>
|
||||
<option value="Polish">Polacco</option>
|
||||
<option value="Czech">Ceco</option>
|
||||
<option value="Icelandic">Islandese</option>
|
||||
<option value="Estonian">Estone</option>
|
||||
<option value="Ukrainian">Ucraino</option>
|
||||
</optgroup>
|
||||
<optgroup label="Asia e Medio Oriente">
|
||||
<option value="Vietnamese">Vietnamita</option>
|
||||
<option value="Thai">Tailandese</option>
|
||||
<option value="Malay">Malese</option>
|
||||
<option value="Indonesian">Indonesiano</option>
|
||||
<option value="Filipino">Filippino / Tagalog</option>
|
||||
<option value="Burmese">Birmano (Myanmar)</option>
|
||||
<option value="Khmer">Khmer / Cambogiano</option>
|
||||
<option value="Persian">Persiano</option>
|
||||
<option value="Hebrew">Ebraico</option>
|
||||
<option value="Hindi">Hindi</option>
|
||||
<option value="Marathi">Marathi</option>
|
||||
<option value="Bengali">Bengali</option>
|
||||
<option value="Tamil">Tamil</option>
|
||||
<option value="Gujarati">Gujarati</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sourceLanguage">Lingua sorgente (opzionale)</label>
|
||||
<select id="sourceLanguage">
|
||||
<option value="">Autodetect</option>
|
||||
<option value="English">Inglese</option>
|
||||
<option value="Italian">Italiano</option>
|
||||
<option value="Spanish">Spagnolo</option>
|
||||
<option value="French">Francese</option>
|
||||
<option value="German">Tedesco</option>
|
||||
<option value="Chinese">Cinese</option>
|
||||
<option value="Japanese">Giapponese</option>
|
||||
<option value="Russian">Russo</option>
|
||||
<option value="Arabic">Arabo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn-primary" id="translateBtn">
|
||||
▶ Traduci
|
||||
</button>
|
||||
<button type="reset" class="btn-secondary">Pulisci</button>
|
||||
</div>
|
||||
|
||||
<div class="status-message" id="statusMessage"></div>
|
||||
</form>
|
||||
|
||||
<div class="result-container" id="resultContainer">
|
||||
<div class="progress-container" id="progressContainer">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
<div class="progress-text">
|
||||
<span id="progressText">Elaborazione: 0%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-box" id="resultBox" style="display: none;">
|
||||
<h3>Testo Tradotto</h3>
|
||||
<p id="translatedText"></p>
|
||||
<button type="button" class="copy-btn" id="copyBtn">📋 Copia</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('translatorForm');
|
||||
const sourceText = document.getElementById('sourceText');
|
||||
const targetLanguage = document.getElementById('targetLanguage');
|
||||
const translateBtn = document.getElementById('translateBtn');
|
||||
const charCount = document.getElementById('charCount');
|
||||
const statusMessage = document.getElementById('statusMessage');
|
||||
const resultContainer = document.getElementById('resultContainer');
|
||||
const progressContainer = document.getElementById('progressContainer');
|
||||
const progressFill = document.getElementById('progressFill');
|
||||
const progressText = document.getElementById('progressText');
|
||||
const resultBox = document.getElementById('resultBox');
|
||||
const translatedTextEl = document.getElementById('translatedText');
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
|
||||
// Update char count
|
||||
sourceText.addEventListener('input', () => {
|
||||
charCount.textContent = sourceText.value.length;
|
||||
});
|
||||
|
||||
// Chunk text into tokens (simple space-based tokenization)
|
||||
function chunkText(text, maxTokens = 500) {
|
||||
const words = text.split(/\s+/).filter(w => w.length > 0);
|
||||
const chunks = [];
|
||||
let currentChunk = [];
|
||||
|
||||
for (const word of words) {
|
||||
currentChunk.push(word);
|
||||
if (currentChunk.length >= maxTokens) {
|
||||
chunks.push(currentChunk.join(' '));
|
||||
currentChunk = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (currentChunk.length > 0) {
|
||||
chunks.push(currentChunk.join(' '));
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// Translate chunk via Ollama
|
||||
async function translateChunk(text, targetLang) {
|
||||
const prompt = `Translate the following text into ${targetLang}, output only the translation without extra explanation:\n\n${text}\n\nTranslation:`;
|
||||
|
||||
const response = await fetch('/api/translate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: prompt,
|
||||
model: 'xieweicong95/HY-MT1.5-1.8B',
|
||||
stream: false
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.translation || data.response || '';
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const text = sourceText.value.trim();
|
||||
const target = targetLanguage.value;
|
||||
|
||||
if (!text) {
|
||||
showStatus('Inserisci un testo da tradurre', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
showStatus('Seleziona una lingua di destinazione', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
translateBtn.disabled = true;
|
||||
resultContainer.classList.remove('show');
|
||||
resultBox.style.display = 'none';
|
||||
progressContainer.classList.add('show');
|
||||
statusMessage.textContent = '';
|
||||
|
||||
try {
|
||||
const chunks = chunkText(text, 500);
|
||||
const translations = [];
|
||||
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const progress = Math.round(((i + 1) / chunks.length) * 100);
|
||||
progressFill.style.width = progress + '%';
|
||||
progressText.textContent = `Elaborazione chunk ${i + 1} di ${chunks.length}...`;
|
||||
|
||||
try {
|
||||
const translation = await translateChunk(chunks[i], target);
|
||||
translations.push(translation.trim());
|
||||
} catch (err) {
|
||||
console.error(`Errore nel chunk ${i + 1}:`, err);
|
||||
translations.push(`[ERRORE NEL CHUNK ${i + 1}]`);
|
||||
}
|
||||
|
||||
// Small delay to avoid overwhelming the server
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const fullTranslation = translations.join(' ').replace(/\s+/g, ' ').trim();
|
||||
translatedTextEl.textContent = fullTranslation;
|
||||
resultBox.style.display = 'block';
|
||||
resultContainer.classList.add('show');
|
||||
showStatus('Traduzione completata!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore:', error);
|
||||
showStatus('Errore durante la traduzione: ' + error.message, 'error');
|
||||
resultContainer.classList.add('show');
|
||||
} finally {
|
||||
translateBtn.disabled = false;
|
||||
progressContainer.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Copy to clipboard
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
const text = translatedTextEl.textContent;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
copyBtn.textContent = '✓ Copiato!';
|
||||
copyBtn.classList.add('copied');
|
||||
setTimeout(() => {
|
||||
copyBtn.textContent = '📋 Copia';
|
||||
copyBtn.classList.remove('copied');
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
showStatus('Errore nella copia', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
function showStatus(message, type = 'info') {
|
||||
statusMessage.textContent = message;
|
||||
statusMessage.className = 'status-message ' + type;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user