Initial commit: SmallWhisper - web app per trascrizione audio con faster-whisper

This commit is contained in:
truetype committed 2026-06-25 20:30:34 +02:00
commit 98b0e8f198
6 files changed
+274

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
# Virtual environment
.venv/
# Python cache
__pycache__/
*.pyc
*.pyo
# Temporary uploaded files
tmp/
# OpenCode config
.opencode/
# Session notes
sess.txt
# Git
.git/
+35
View File
@@ -0,0 +1,35 @@
# SmallWhisper
Web app per trascrizione audio in italiano usando **faster-whisper**.
Carica un file audio (`.m4a`, `.mp3`, `.wav`, `.ogg`, ecc.) e ottieni la trascrizione in formato testo — tutto in locale, senza chiamate a servizi esterni.
## Requisiti
- Python 3.10+
- ffmpeg (`apt install ffmpeg` su Debian/Ubuntu)
- ~2 GB di spazio per il modello Whisper
## Installazione
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
## Avvio
```bash
source .venv/bin/activate
export HF_TOKEN=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
python app.py
```
Apri il browser su `http://localhost:5000`.
### Note
- Il modello Whisper `small` (~1.5 GB) viene scaricato automaticamente al primo avvio.
- La prima trascrizione potrebbe essere più lenta, poi il modello resta in cache.
- Supporta qualsiasi formato audio gestito da ffmpeg.
+126
View File
@@ -0,0 +1,126 @@
import os
import uuid
import threading
from flask import Flask, request, render_template, send_file, url_for
from faster_whisper import WhisperModel
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100 MB limit
app.secret_key = os.urandom(24)
# ── Whisper model ───────────────────────────────────────────────
# Modello small (1.5 GB) — ottimo per italiano, veloce su CPU.
# Cambia in "base" o "tiny" se vuoi più velocità, "medium" per più accuratezza.
WHISPER_MODEL = "small"
print(f"[INFO] Caricamento modello Whisper '{WHISPER_MODEL}'...")
whisper = WhisperModel(WHISPER_MODEL, device="cpu", compute_type="int8")
print("[INFO] Modello caricato!")
# ── Job tracking (in memoria) ───────────────────────────────────
jobs = {}
# ── Routes ──────────────────────────────────────────────────────
@app.route('/')
def index():
return render_template('index.html')
@app.route('/upload', methods=['POST'])
def upload():
if 'audio' not in request.files:
return {'error': 'Nessun file audio'}, 400
file = request.files['audio']
if file.filename == '':
return {'error': 'Nome file vuoto'}, 400
job_id = str(uuid.uuid4())
os.makedirs('tmp', exist_ok=True)
tmp_path = os.path.join('tmp', f'{job_id}.m4a')
file.save(tmp_path)
jobs[job_id] = {
'status': 'running',
'progress': 0,
'message': 'Caricato',
'download': None,
}
threading.Thread(target=job_worker, args=(job_id, tmp_path), daemon=True).start()
return {'statusUrl': url_for('status', job_id=job_id)}
@app.route('/status/<job_id>')
def status(job_id):
job = jobs.get(job_id)
if not job:
return {'error': 'Job non trovato'}, 404
# Mentre whisper lavora, mostriamo "Trascrizione in corso..." con progress 50
if job.get('status') == 'running' and job.get('progress', 0) < 100:
return {
'status': 'running',
'progress': 50,
'message': 'Trascrizione in corso...',
'download': None,
}
return {
'status': job.get('status'),
'progress': job.get('progress', 100),
'message': job.get('message'),
'download': job.get('download'),
}
@app.route('/download/<job_id>.txt')
def download(job_id):
txt_path = os.path.join('tmp', f'trascrizione_{job_id}.txt')
if not os.path.exists(txt_path):
return {'error': 'File non trovato'}, 404
return send_file(
txt_path,
as_attachment=True,
download_name='trascrizione.txt',
mimetype='text/plain',
)
# ── Worker (thread background) ──────────────────────────────────
def job_worker(job_id, tmp_path):
try:
jobs[job_id]['message'] = 'Trascrizione in corso...'
# Whisper trascrive direttamente dal file (supporta .m4a via ffmpeg)
segments, info = whisper.transcribe(tmp_path, language="it", beam_size=5)
transcript_parts = []
for segment in segments:
transcript_parts.append(segment.text.strip())
transcription = "\n".join(transcript_parts)
txt_path = os.path.join('tmp', f'trascrizione_{job_id}.txt')
with open(txt_path, 'w', encoding='utf-8') as f:
f.write(transcription)
jobs[job_id]['download'] = f'/download/{job_id}.txt'
jobs[job_id]['message'] = 'Completato'
jobs[job_id]['progress'] = 100
jobs[job_id]['status'] = 'done'
except Exception as e:
jobs[job_id]['status'] = 'error'
jobs[job_id]['message'] = str(e)
finally:
try:
os.remove(tmp_path)
except Exception:
pass
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
+8
View File
@@ -0,0 +1,8 @@
"""Shim module to satisfy pydub's optional import of `pyaudioop`.
It simply re-exports the standard library `audioop` functions.
"""
import audioop as _audioop
# Export everything that does not start with an underscore
for _name in dir(_audioop):
if not _name.startswith('_'):
globals()[_name] = getattr(_audioop, _name)
+2
View File
@@ -0,0 +1,2 @@
Flask==3.0.3
faster-whisper==1.2.1
+84
View File
@@ -0,0 +1,84 @@
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8" />
<title>Trascrizione Audio</title>
<style>
body {font-family: Arial, sans-serif; margin: 2rem;}
.container {max-width: 600px; margin: auto;}
.message {color: red; margin-bottom: 1rem;}
.progress-container {margin-top: 1rem;}
progress {width: 100%; height: 20px;}
.status {margin-top: 0.5rem; color: #555;}
</style>
</head>
<body>
<div class="container">
<h1>Carica un file audio</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="message">{{ messages[0] }}</div>
{% endif %}
{% endwith %}
<form id="uploadForm" method="post" enctype="multipart/form-data">
<input type="file" name="audio" accept="audio/*" required />
<br/><br/>
<button type="submit">Trascrivi</button>
</form>
<p class="hint">La trascrizione verrà restituita come file <code>trascrizione.txt</code>.</p>
</div>
<script>
document.getElementById('uploadForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData();
formData.append('audio', document.querySelector('input[type="file"]').files[0]);
// Mostra la finestra di processing
document.querySelector('.container').innerHTML = '<h2>Elaborazione in corso...</h2>' +
'<div class="progress-container">' +
'<progress id="progressBar" value="0" max="100"></progress>' +
'<div class="status" id="statusText">Sto caricando il file...</div>' +
'</div>';
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.statusUrl) {
checkStatus(data.statusUrl);
} else if (data.download) {
window.location.href = data.download;
}
})
.catch(err => {
document.getElementById('statusText').textContent = 'Errore: ' + err;
});
function checkStatus(url) {
fetch(url)
.then(response => response.json())
.then(data => {
const progress = data.progress || 0;
const progressBar = document.getElementById('progressBar');
const statusText = document.getElementById('statusText');
progressBar.value = progress;
statusText.textContent = data.message || 'Elaborazione in corso...';
if (progress < 100) {
setTimeout(() => checkStatus(url), 1000);
} else if (data.download) {
statusText.textContent = 'Completato! Scarico il file...';
window.location.href = data.download;
}
})
.catch(err => {
document.getElementById('statusText').textContent = 'Errore nel recupero stato: ' + err;
});
}
});
</script>
</body>
</html>