commit 98b0e8f198a02b5b9afaaf3b2b4688e60dc6d7c4 Author: truetype Date: Thu Jun 25 20:30:34 2026 +0200 Initial commit: SmallWhisper - web app per trascrizione audio con faster-whisper diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c60f74 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..992bb03 --- /dev/null +++ b/README.md @@ -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. diff --git a/app.py b/app.py new file mode 100644 index 0000000..3c5ecd3 --- /dev/null +++ b/app.py @@ -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/') +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/.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) diff --git a/pyaudioop.py b/pyaudioop.py new file mode 100644 index 0000000..46b0727 --- /dev/null +++ b/pyaudioop.py @@ -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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b3015c4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.0.3 +faster-whisper==1.2.1 \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..3c69174 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,84 @@ + + + + + Trascrizione Audio + + + +
+

Carica un file audio

+ {% with messages = get_flashed_messages() %} + {% if messages %} +
{{ messages[0] }}
+ {% endif %} + {% endwith %} +
+ +

+ +
+

La trascrizione verrà restituita come file trascrizione.txt.

+
+ + + + \ No newline at end of file