Initial commit: SmallWhisper - web app per trascrizione audio con faster-whisper
This commit is contained in:
commit
98b0e8f198
6 files changed
+274
No files matched your search
+19
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
Flask==3.0.3
|
||||
faster-whisper==1.2.1
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user