Notebook di trascrizione MP3 con Whisper su Colab
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
2aff8cde8c
3 files changed
+535
No files matched your search
@@ -0,0 +1,2 @@
|
||||
*.mp3
|
||||
*.zip
|
||||
@@ -0,0 +1,52 @@
|
||||
# Trascrizione MP3 con Whisper su Google Colab
|
||||
|
||||
Notebook Jupyter che trascrive file audio (MP3, WAV, M4A, FLAC, OGG, OPUS, AAC) usando [faster-whisper](https://github.com/SYSTRAN/faster-whisper) e `large-v3` sulla GPU di Google Colab. Nessuna API a pagamento né token Hugging Face: il modello è pubblico e l'audio è elaborato nella macchina Colab.
|
||||
|
||||
## Output
|
||||
|
||||
Per ogni audio, raccolti in uno ZIP:
|
||||
|
||||
| File | Contenuto |
|
||||
|---|---|
|
||||
| `.md` | Testo con timestamp, sezioni temporali e segnalazioni "da riascoltare" |
|
||||
| `.txt` | Solo il testo riconosciuto |
|
||||
| `.srt` | Sottotitoli |
|
||||
| `.json` | Segmenti, timestamp e probabilità per parola |
|
||||
| `esecuzione.log`, `riepilogo.json` | Log ed esito di ogni file |
|
||||
|
||||
La trascrizione è automatica: non fa riassunti e non identifica gli interlocutori.
|
||||
|
||||
## Uso
|
||||
|
||||
1. Apri [Google Colab](https://colab.research.google.com), scegli **File → Carica notebook** e seleziona `trascrizione_mp3_whisper_colab.ipynb`.
|
||||
2. **Runtime → Cambia tipo di runtime → GPU T4**.
|
||||
3. Modifica le opzioni nella prima cella (vedi sotto).
|
||||
4. **Runtime → Esegui tutto** e carica gli MP3 quando compare **Scegli file**.
|
||||
5. Al termine lo ZIP si scarica automaticamente.
|
||||
|
||||
Senza GPU il notebook si ferma con un messaggio, invece di avviare una trascrizione lenta su CPU.
|
||||
|
||||
## Opzioni (cella 1)
|
||||
|
||||
| Variabile | Default | Descrizione |
|
||||
|---|---|---|
|
||||
| `MODELLO` | `large-v3` | `large-v3` per la qualità, `turbo` per la velocità |
|
||||
| `LINGUA` | `it` | Codice lingua, oppure `None` per il rilevamento automatico |
|
||||
| `PRECISIONE` | `float16` | `int8_float16` usa meno memoria GPU |
|
||||
| `CONTESTO` | `""` | Nomi o termini difficili, es. `"Kiev, Chisinau"`; riduce gli errori sui nomi propri |
|
||||
| `ORIGINE` | `upload` | `upload` carica dal PC, `drive` legge da una cartella di Google Drive |
|
||||
| `CARTELLA_DRIVE` | `/content/drive/MyDrive/Audio da trascrivere` | Cartella di input con `ORIGINE = "drive"` |
|
||||
| `SALVA_SU_DRIVE` | `False` | Copia i risultati anche su Drive |
|
||||
| `FILTRO_SILENZI` | `False` | Attiva il filtro VAD sul non-parlato |
|
||||
| `MINUTI_PER_SEZIONE` | `5` | Durata delle sezioni nel Markdown |
|
||||
| `SOGLIA_PAROLA_INCERTA` | `0.45` | Sotto questa probabilità la parola è segnalata |
|
||||
| `SCARICA_ZIP` | `True` | Scarica lo ZIP a fine esecuzione |
|
||||
|
||||
Per audio lunghi usa `ORIGINE = "drive"` e `SALVA_SU_DRIVE = True`: i file in `/content` sono temporanei.
|
||||
|
||||
## Note
|
||||
|
||||
- Il notebook usa `google.colab` (upload, Drive): va eseguito su Colab. Con l'estensione Colab di VS Code il pulsante di upload potrebbe non comparire; usa Drive.
|
||||
- Il warning sull'HF Hub non autenticato è innocuo.
|
||||
- Memoria GPU insufficiente: `PRECISIONE = "int8_float16"` o `MODELLO = "turbo"`.
|
||||
- Riascolta i punti segnalati: i nomi propri possono essere sbagliati anche senza segnalazione.
|
||||
@@ -0,0 +1,481 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell-000",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Trascrizione MP3 su Google Colab — GPU Tesla T4\n",
|
||||
"\n",
|
||||
"Trascrive uno o più file senza riassumere. Produce **Markdown, TXT, SRT e JSON**, raccolti in uno ZIP.\n",
|
||||
"\n",
|
||||
"1. Seleziona **Runtime → Cambia tipo di runtime → GPU T4**, se disponibile.\n",
|
||||
"2. Regola le opzioni nella prima cella (italiano e `large-v3` sono preimpostati). In `CONTESTO` puoi indicare nomi e termini difficili (es. luoghi, persone) per ridurre gli errori.\n",
|
||||
"3. Scegli **Runtime → Esegui tutto**, quindi carica gli MP3 quando richiesto.\n",
|
||||
"4. Al termine scarica lo ZIP. Puoi anche leggere i risultati nella cartella indicata.\n",
|
||||
"\n",
|
||||
"Il modello viene scaricato al primo avvio. L'audio viene elaborato nella macchina Colab di Google, non sul tuo PC. Non occorre una chiave API. La disponibilità della T4 dipende da Colab.\n",
|
||||
"\n",
|
||||
"**Limiti:** la trascrizione è automatica e può perdere parole o introdurre errori. Le segnalazioni di bassa affidabilità non dimostrano che un passaggio sia incomprensibile. Non viene eseguita l'identificazione delle voci: nessun nome o cambio di interlocutore viene inventato. Le sezioni Markdown sono intervalli temporali, non riassunti.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "cell-001",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 1. Opzioni\n",
|
||||
"MODELLO = \"large-v3\" # \"large-v3\" per qualità; \"turbo\" per maggiore velocità\n",
|
||||
"LINGUA = \"it\" # \"it\" per italiano; None per rilevamento automatico\n",
|
||||
"PRECISIONE = \"float16\" # adatta alla T4; \"int8_float16\" usa meno memoria\n",
|
||||
"CONTESTO = \"\" # nomi/termini da riconoscere bene, es. \"Kiev, Chisinau, Bucarest\"\n",
|
||||
"ORIGINE = \"upload\" # \"upload\" oppure \"drive\"\n",
|
||||
"CARTELLA_DRIVE = \"/content/drive/MyDrive/Audio da trascrivere\"\n",
|
||||
"SALVA_SU_DRIVE = False # True per conservare i risultati anche su Drive\n",
|
||||
"CARTELLA_RISULTATI_DRIVE = \"/content/drive/MyDrive/Trascrizioni\"\n",
|
||||
"FILTRO_SILENZI = False # False: nessun filtro VAD; True: filtra il non-parlato stimato\n",
|
||||
"MINUTI_PER_SEZIONE = 5\n",
|
||||
"SOGLIA_PAROLA_INCERTA = 0.45\n",
|
||||
"SCARICA_ZIP = True\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell-002",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Installazione e verifica della GPU\n",
|
||||
"La cella verifica la GPU realmente assegnata. Se non c'è una GPU, si ferma con istruzioni invece di avviare una lenta trascrizione sulla CPU.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "cell-003",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"GPU assegnata: Tesla T4, 15360 MiB\n",
|
||||
"Installazione completata.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# 2. Dipendenze\n",
|
||||
"import subprocess, sys\n",
|
||||
"gpu = subprocess.run([\"nvidia-smi\", \"--query-gpu=name,memory.total\", \"--format=csv,noheader\"],\n",
|
||||
" capture_output=True, text=True) if __import__(\"shutil\").which(\"nvidia-smi\") else None\n",
|
||||
"if gpu is None or gpu.returncode != 0:\n",
|
||||
" raise RuntimeError(\"GPU assente. Seleziona Runtime > Cambia tipo di runtime > GPU T4, poi riesegui tutto.\")\n",
|
||||
"print(\"GPU assegnata:\", gpu.stdout.strip())\n",
|
||||
"if \"T4\" not in gpu.stdout:\n",
|
||||
" print(\"Colab ha assegnato una GPU diversa dalla T4; il notebook userà quella disponibile.\")\n",
|
||||
"subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"-q\",\n",
|
||||
" \"faster-whisper==1.2.1\", \"ctranslate2>=4.5,<5\", \"av>=11,<16\",\n",
|
||||
" \"nvidia-cublas-cu12\", \"nvidia-cudnn-cu12>=9,<10\"])\n",
|
||||
"print(\"Installazione completata.\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell-004",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Caricamento degli audio\n",
|
||||
"Con `upload` puoi selezionare più MP3. Per file grandi usa `drive`: autorizza il collegamento a Google Drive e indica la cartella nelle opzioni. Sono accettati anche WAV, M4A, FLAC, OGG, OPUS e AAC. I file di Drive sono letti senza modificarli.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell-005",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"\n",
|
||||
" <input type=\"file\" id=\"files-c3bb888e-e70c-4287-a754-e4e1a27b4503\" name=\"files[]\" multiple disabled\n",
|
||||
" style=\"border:none\" />\n",
|
||||
" <output id=\"result-c3bb888e-e70c-4287-a754-e4e1a27b4503\">\n",
|
||||
" Upload widget is only available when the cell has been executed in the\n",
|
||||
" current browser session. Please rerun this cell to enable.\n",
|
||||
" </output>\n",
|
||||
" <script>// Copyright 2017 Google LLC\n",
|
||||
"//\n",
|
||||
"// Licensed under the Apache License, Version 2.0 (the \"License\");\n",
|
||||
"// you may not use this file except in compliance with the License.\n",
|
||||
"// You may obtain a copy of the License at\n",
|
||||
"//\n",
|
||||
"// http://www.apache.org/licenses/LICENSE-2.0\n",
|
||||
"//\n",
|
||||
"// Unless required by applicable law or agreed to in writing, software\n",
|
||||
"// distributed under the License is distributed on an \"AS IS\" BASIS,\n",
|
||||
"// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
|
||||
"// See the License for the specific language governing permissions and\n",
|
||||
"// limitations under the License.\n",
|
||||
"\n",
|
||||
"/**\n",
|
||||
" * @fileoverview Helpers for google.colab Python module.\n",
|
||||
" */\n",
|
||||
"(function(scope) {\n",
|
||||
"function span(text, styleAttributes = {}) {\n",
|
||||
" const element = document.createElement('span');\n",
|
||||
" element.textContent = text;\n",
|
||||
" for (const key of Object.keys(styleAttributes)) {\n",
|
||||
" element.style[key] = styleAttributes[key];\n",
|
||||
" }\n",
|
||||
" return element;\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"// Max number of bytes which will be uploaded at a time.\n",
|
||||
"const MAX_PAYLOAD_SIZE = 100 * 1024;\n",
|
||||
"\n",
|
||||
"function _uploadFiles(inputId, outputId) {\n",
|
||||
" const steps = uploadFilesStep(inputId, outputId);\n",
|
||||
" const outputElement = document.getElementById(outputId);\n",
|
||||
" // Cache steps on the outputElement to make it available for the next call\n",
|
||||
" // to uploadFilesContinue from Python.\n",
|
||||
" outputElement.steps = steps;\n",
|
||||
"\n",
|
||||
" return _uploadFilesContinue(outputId);\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"// This is roughly an async generator (not supported in the browser yet),\n",
|
||||
"// where there are multiple asynchronous steps and the Python side is going\n",
|
||||
"// to poll for completion of each step.\n",
|
||||
"// This uses a Promise to block the python side on completion of each step,\n",
|
||||
"// then passes the result of the previous step as the input to the next step.\n",
|
||||
"function _uploadFilesContinue(outputId) {\n",
|
||||
" const outputElement = document.getElementById(outputId);\n",
|
||||
" const steps = outputElement.steps;\n",
|
||||
"\n",
|
||||
" const next = steps.next(outputElement.lastPromiseValue);\n",
|
||||
" return Promise.resolve(next.value.promise).then((value) => {\n",
|
||||
" // Cache the last promise value to make it available to the next\n",
|
||||
" // step of the generator.\n",
|
||||
" outputElement.lastPromiseValue = value;\n",
|
||||
" return next.value.response;\n",
|
||||
" });\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"/**\n",
|
||||
" * Generator function which is called between each async step of the upload\n",
|
||||
" * process.\n",
|
||||
" * @param {string} inputId Element ID of the input file picker element.\n",
|
||||
" * @param {string} outputId Element ID of the output display.\n",
|
||||
" * @return {!Iterable<!Object>} Iterable of next steps.\n",
|
||||
" */\n",
|
||||
"function* uploadFilesStep(inputId, outputId) {\n",
|
||||
" const inputElement = document.getElementById(inputId);\n",
|
||||
" inputElement.disabled = false;\n",
|
||||
"\n",
|
||||
" const outputElement = document.getElementById(outputId);\n",
|
||||
" outputElement.innerHTML = '';\n",
|
||||
"\n",
|
||||
" const pickedPromise = new Promise((resolve) => {\n",
|
||||
" inputElement.addEventListener('change', (e) => {\n",
|
||||
" resolve(e.target.files);\n",
|
||||
" });\n",
|
||||
" });\n",
|
||||
"\n",
|
||||
" const cancel = document.createElement('button');\n",
|
||||
" inputElement.parentElement.appendChild(cancel);\n",
|
||||
" cancel.textContent = 'Cancel upload';\n",
|
||||
" const cancelPromise = new Promise((resolve) => {\n",
|
||||
" cancel.onclick = () => {\n",
|
||||
" resolve(null);\n",
|
||||
" };\n",
|
||||
" });\n",
|
||||
"\n",
|
||||
" // Wait for the user to pick the files.\n",
|
||||
" const files = yield {\n",
|
||||
" promise: Promise.race([pickedPromise, cancelPromise]),\n",
|
||||
" response: {\n",
|
||||
" action: 'starting',\n",
|
||||
" }\n",
|
||||
" };\n",
|
||||
"\n",
|
||||
" cancel.remove();\n",
|
||||
"\n",
|
||||
" // Disable the input element since further picks are not allowed.\n",
|
||||
" inputElement.disabled = true;\n",
|
||||
"\n",
|
||||
" if (!files) {\n",
|
||||
" return {\n",
|
||||
" response: {\n",
|
||||
" action: 'complete',\n",
|
||||
" }\n",
|
||||
" };\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" for (const file of files) {\n",
|
||||
" const li = document.createElement('li');\n",
|
||||
" li.append(span(file.name, {fontWeight: 'bold'}));\n",
|
||||
" li.append(span(\n",
|
||||
" `(${file.type || 'n/a'}) - ${file.size} bytes, ` +\n",
|
||||
" `last modified: ${\n",
|
||||
" file.lastModifiedDate ? file.lastModifiedDate.toLocaleDateString() :\n",
|
||||
" 'n/a'} - `));\n",
|
||||
" const percent = span('0% done');\n",
|
||||
" li.appendChild(percent);\n",
|
||||
"\n",
|
||||
" outputElement.appendChild(li);\n",
|
||||
"\n",
|
||||
" const fileDataPromise = new Promise((resolve) => {\n",
|
||||
" const reader = new FileReader();\n",
|
||||
" reader.onload = (e) => {\n",
|
||||
" resolve(e.target.result);\n",
|
||||
" };\n",
|
||||
" reader.readAsArrayBuffer(file);\n",
|
||||
" });\n",
|
||||
" // Wait for the data to be ready.\n",
|
||||
" let fileData = yield {\n",
|
||||
" promise: fileDataPromise,\n",
|
||||
" response: {\n",
|
||||
" action: 'continue',\n",
|
||||
" }\n",
|
||||
" };\n",
|
||||
"\n",
|
||||
" // Use a chunked sending to avoid message size limits. See b/62115660.\n",
|
||||
" let position = 0;\n",
|
||||
" do {\n",
|
||||
" const length = Math.min(fileData.byteLength - position, MAX_PAYLOAD_SIZE);\n",
|
||||
" const chunk = new Uint8Array(fileData, position, length);\n",
|
||||
" position += length;\n",
|
||||
"\n",
|
||||
" const base64 = btoa(String.fromCharCode.apply(null, chunk));\n",
|
||||
" yield {\n",
|
||||
" response: {\n",
|
||||
" action: 'append',\n",
|
||||
" file: file.name,\n",
|
||||
" data: base64,\n",
|
||||
" },\n",
|
||||
" };\n",
|
||||
"\n",
|
||||
" let percentDone = fileData.byteLength === 0 ?\n",
|
||||
" 100 :\n",
|
||||
" Math.round((position / fileData.byteLength) * 100);\n",
|
||||
" percent.textContent = `${percentDone}% done`;\n",
|
||||
"\n",
|
||||
" } while (position < fileData.byteLength);\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" // All done.\n",
|
||||
" yield {\n",
|
||||
" response: {\n",
|
||||
" action: 'complete',\n",
|
||||
" }\n",
|
||||
" };\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"scope.google = scope.google || {};\n",
|
||||
"scope.google.colab = scope.google.colab || {};\n",
|
||||
"scope.google.colab._files = {\n",
|
||||
" _uploadFiles,\n",
|
||||
" _uploadFilesContinue,\n",
|
||||
"};\n",
|
||||
"})(self);\n",
|
||||
"</script> "
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.core.display.HTML object>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# 3. Sorgenti e cartelle\n",
|
||||
"from pathlib import Path\n",
|
||||
"from datetime import datetime, timezone\n",
|
||||
"import json, os, shutil\n",
|
||||
"from google.colab import files, drive\n",
|
||||
"if ORIGINE not in {\"upload\", \"drive\"}:\n",
|
||||
" raise ValueError(\"ORIGINE deve essere 'upload' oppure 'drive'.\")\n",
|
||||
"if ORIGINE == \"drive\" or SALVA_SU_DRIVE:\n",
|
||||
" drive.mount(\"/content/drive\")\n",
|
||||
"RUN_ID = datetime.now(timezone.utc).strftime(\"%Y%m%d_%H%M%S_%f\")\n",
|
||||
"RUN_DIR = Path(\"/content/trascrizioni\") / RUN_ID\n",
|
||||
"RUN_DIR.mkdir(parents=True, exist_ok=True)\n",
|
||||
"if ORIGINE == \"upload\":\n",
|
||||
" INPUT_DIR = Path(\"/content/audio_caricati\") / RUN_ID\n",
|
||||
" INPUT_DIR.mkdir(parents=True, exist_ok=True)\n",
|
||||
" old_cwd = Path.cwd()\n",
|
||||
" try:\n",
|
||||
" os.chdir(INPUT_DIR)\n",
|
||||
" uploaded = files.upload()\n",
|
||||
" finally:\n",
|
||||
" os.chdir(old_cwd)\n",
|
||||
" del uploaded # libera la copia in memoria\n",
|
||||
"else:\n",
|
||||
" INPUT_DIR = Path(CARTELLA_DRIVE)\n",
|
||||
"if not INPUT_DIR.is_dir():\n",
|
||||
" raise FileNotFoundError(f\"Cartella non trovata: {INPUT_DIR}\")\n",
|
||||
"extensions = {\".mp3\", \".wav\", \".m4a\", \".flac\", \".ogg\", \".opus\", \".aac\"}\n",
|
||||
"AUDIO_FILES = sorted(p for p in INPUT_DIR.iterdir() if p.is_file() and p.suffix.lower() in extensions)\n",
|
||||
"if not AUDIO_FILES:\n",
|
||||
" raise ValueError(\"Nessun audio supportato nella cartella selezionata.\")\n",
|
||||
"if MINUTI_PER_SEZIONE <= 0 or not 0 <= SOGLIA_PAROLA_INCERTA <= 1:\n",
|
||||
" raise ValueError(\"Controlla MINUTI_PER_SEZIONE e SOGLIA_PAROLA_INCERTA.\")\n",
|
||||
"for p in AUDIO_FILES:\n",
|
||||
" print(f\"• {p.name} ({p.stat().st_size / 1024**2:.1f} MB)\")\n",
|
||||
"print(\"Risultati:\", RUN_DIR)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell-006",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Trascrizione sulla GPU\n",
|
||||
"Il modello viene caricato una volta e riutilizzato per tutti gli audio. Ogni segmento viene salvato progressivamente: in caso di errore rimane un file `.partial.jsonl`, che **non** va confuso con una trascrizione completa.\n",
|
||||
"\n",
|
||||
"La procedura lavora in un processo separato, configurando prima le librerie CUDA e cuDNN. Non è necessario riavviare il runtime dopo l'installazione. I messaggi mostrano la posizione raggiunta nell'audio.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell-007",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 4. Prepara il programma di trascrizione\n",
|
||||
"WORKER_SOURCE = 'import os, sys, json, traceback, re\\nfrom pathlib import Path\\nfrom dataclasses import asdict\\nfrom importlib.metadata import version\\nfrom faster_whisper import WhisperModel\\nimport ctranslate2\\n\\ndef timestamp(seconds, srt=False):\\n ms = max(0, round(seconds * 1000))\\n h, ms = divmod(ms, 3600000)\\n m, ms = divmod(ms, 60000)\\n s, ms = divmod(ms, 1000)\\n return f\"{h:02}:{m:02}:{s:02}\" + (f\",{ms:03}\" if srt else \"\")\\n\\ndef export_result(source, base, segments, info, cfg):\\n md = [f\"# Trascrizione — {source.name}\", \"\",\\n f\"**Lingua rilevata/impostata:** {info.language} \",\\n f\"**Durata:** {timestamp(info.duration)} \",\\n f\"**Modello:** {cfg[\\'model\\']} \",\\n \"**Interlocutori:** non identificati automaticamente.\", \"\",\\n \"> Trascrizione automatica, non verificata mediante ascolto. Nessun riassunto. \"\\n \"Le parole dubbie sono conservate e segnalate, non sostituite con ipotesi. \"\\n \"I timestamp e i segnali di affidabilità sono approssimativi.\", \"\"]\\n txt, srt = [], []\\n previous_section = None\\n for i, seg in enumerate(segments, 1):\\n text = seg[\\'text\\'].strip()\\n section = int(seg[\\'start\\'] // (cfg[\\'section_minutes\\'] * 60))\\n if section != previous_section:\\n start = section * cfg[\\'section_minutes\\'] * 60\\n end = min(start + cfg[\\'section_minutes\\'] * 60, info.duration)\\n md += [f\"## {timestamp(start)}–{timestamp(end)}\", \"\"]\\n previous_section = section\\n md += [f\"**[{timestamp(seg[\\'start\\'])}–{timestamp(seg[\\'end\\'])}]**\", \"\", text, \"\"]\\n uncertain = [w for w in (seg.get(\\'words\\') or []) if w[\\'probability\\'] < cfg[\\'threshold\\']]\\n if uncertain:\\n detail = \"; \".join(f\"{timestamp(w[\\'start\\'])}: «{w[\\'word\\'].strip()}»\" for w in uncertain)\\n md += [f\"*[Da riascoltare: letture automatiche incerte — {detail}]*\", \"\"]\\n if seg.get(\\'avg_logprob\\', 0) < -1 or seg.get(\\'compression_ratio\\', 0) > 2.4:\\n md += [\"*[Segmento da verificare: possibile riconoscimento errato o ripetitivo.]*\", \"\"]\\n txt.append(text)\\n srt.append(f\"{i}\\\\n{timestamp(seg[\\'start\\'], True)} --> {timestamp(seg[\\'end\\'], True)}\\\\n{text}\\\\n\")\\n if not segments:\\n md += [\"[Nessun parlato riconosciuto automaticamente: verificare l’audio.]\", \"\"]\\n for ext, content in [(\\'md\\', \\'\\\\n\\'.join(md)), (\\'txt\\', \\'\\\\n\\'.join(txt)), (\\'srt\\', \\'\\\\n\\'.join(srt))]:\\n base.with_suffix(\\'.\\' + ext).write_text(content, encoding=\\'utf-8\\')\\n metadata = {\\'source\\': str(source), \\'duration_seconds\\': info.duration,\\n \\'language\\': info.language, \\'language_probability\\': info.language_probability,\\n \\'settings\\': cfg, \\'segments\\': segments,\\n \\'versions\\': {p: version(p) for p in [\\'faster-whisper\\',\\'ctranslate2\\',\\'av\\']}}\\n base.with_suffix(\\'.json\\').write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding=\\'utf-8\\')\\n\\ndef main():\\n cfg = json.loads(Path(sys.argv[1]).read_text(encoding=\\'utf-8\\'))\\n if ctranslate2.get_cuda_device_count() < 1:\\n raise RuntimeError(\\'GPU CUDA non utilizzabile. Verifica il runtime Colab.\\')\\n print(\\'Caricamento modello \\' + cfg[\\'model\\'] + \\' sulla GPU...\\', flush=True)\\n model = WhisperModel(cfg[\\'model\\'], device=\\'cuda\\', compute_type=cfg[\\'precision\\'])\\n results=[]\\n for index, name in enumerate(cfg[\\'files\\'], 1):\\n source = Path(name)\\n # Prefix and suffix protect different inputs with the same stem.\\n safe = re.sub(r\\'[^\\\\w\\\\- ]\\', \\'_\\', source.stem)[:100] or \\'audio\\'\\n base = Path(cfg[\\'output\\']) / f\\'{index:03}_{safe}_trascrizione\\'\\n print(f\\'\\\\n[{index}/{len(cfg[\"files\"])}] {source.name}\\', flush=True)\\n partial = base.with_suffix(\\'.partial.jsonl\\')\\n try:\\n iterator, info = model.transcribe(str(source), language=cfg[\\'language\\'],\\n task=\\'transcribe\\', beam_size=5, word_timestamps=True,\\n vad_filter=cfg[\\'vad\\'], hotwords=cfg[\\'context\\'] or None, condition_on_previous_text=False)\\n segments=[]\\n with partial.open(\\'w\\', encoding=\\'utf-8\\') as f:\\n for segment in iterator:\\n record=asdict(segment)\\n segments.append(record)\\n f.write(json.dumps(record, ensure_ascii=False)+\\'\\\\n\\'); f.flush()\\n print(f\\' {timestamp(segment.end)} / {timestamp(info.duration)}\\', flush=True)\\n export_result(source, base, segments, info, cfg)\\n partial.unlinkLine truncated
|
||||
"WORKER = Path(\"/content/trascrivi_gpu.py\")\n",
|
||||
"WORKER.write_text(WORKER_SOURCE, encoding=\"utf-8\")\n",
|
||||
"CONFIG = {\n",
|
||||
" \"model\": MODELLO, \"language\": LINGUA, \"precision\": PRECISIONE,\n",
|
||||
" \"vad\": FILTRO_SILENZI, \"context\": CONTESTO.strip(), \"section_minutes\": MINUTI_PER_SEZIONE,\n",
|
||||
" \"threshold\": SOGLIA_PAROLA_INCERTA,\n",
|
||||
" \"files\": [str(p) for p in AUDIO_FILES], \"output\": str(RUN_DIR)\n",
|
||||
"}\n",
|
||||
"CONFIG_PATH = RUN_DIR / \"configurazione.json\"\n",
|
||||
"CONFIG_PATH.write_text(json.dumps(CONFIG, ensure_ascii=False, indent=2), encoding=\"utf-8\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell-008",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 5. Esegui il riconoscimento e conserva il log\n",
|
||||
"import site\n",
|
||||
"library_dirs = []\n",
|
||||
"for folder in site.getsitepackages():\n",
|
||||
" for package in (\"cublas\", \"cudnn\"):\n",
|
||||
" candidate = Path(folder) / \"nvidia\" / package / \"lib\"\n",
|
||||
" if candidate.is_dir():\n",
|
||||
" library_dirs.append(str(candidate))\n",
|
||||
"if len(library_dirs) < 2:\n",
|
||||
" raise RuntimeError(\"Librerie CUDA/cuDNN non trovate: riesegui la cella 2.\")\n",
|
||||
"env = os.environ.copy()\n",
|
||||
"env[\"LD_LIBRARY_PATH\"] = \":\".join(library_dirs + [env.get(\"LD_LIBRARY_PATH\", \"\")])\n",
|
||||
"env[\"PYTHONUNBUFFERED\"] = \"1\"\n",
|
||||
"with (RUN_DIR / \"esecuzione.log\").open(\"w\", encoding=\"utf-8\") as log:\n",
|
||||
" process = subprocess.Popen([sys.executable, \"-u\", str(WORKER), str(CONFIG_PATH)],\n",
|
||||
" env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n",
|
||||
" text=True, bufsize=1)\n",
|
||||
" try:\n",
|
||||
" for line in process.stdout:\n",
|
||||
" print(line, end=\"\")\n",
|
||||
" log.write(line); log.flush()\n",
|
||||
" returncode = process.wait()\n",
|
||||
" except KeyboardInterrupt:\n",
|
||||
" process.terminate()\n",
|
||||
" process.wait()\n",
|
||||
" print(\"Interrotto: i segmenti già elaborati restano nei file partial.jsonl.\")\n",
|
||||
" raise\n",
|
||||
"if returncode:\n",
|
||||
" print(f\"Processo terminato con codice {returncode}. Controlla il log e gli eventuali errori nello ZIP.\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell-009",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Scarica i risultati\n",
|
||||
"Il Markdown include timestamp, sezioni temporali e letture da controllare. TXT e SRT mantengono il testo riconosciuto senza le annotazioni editoriali; il JSON conserva anche i timestamp e i punteggi delle parole.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell-010",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 6. Archivio ZIP e copia facoltativa su Drive\n",
|
||||
"archive = Path(shutil.make_archive(str(RUN_DIR) + \"_risultati\", \"zip\", root_dir=RUN_DIR))\n",
|
||||
"if SALVA_SU_DRIVE:\n",
|
||||
" destination = Path(CARTELLA_RISULTATI_DRIVE) / RUN_ID\n",
|
||||
" destination.mkdir(parents=True, exist_ok=True)\n",
|
||||
" shutil.copytree(RUN_DIR, destination, dirs_exist_ok=True)\n",
|
||||
" shutil.copy2(archive, destination / archive.name)\n",
|
||||
" print(\"Copia salvata su Drive:\", destination)\n",
|
||||
"print(\"Archivio:\", archive)\n",
|
||||
"print(\"Trascrizioni Markdown:\")\n",
|
||||
"for p in sorted(RUN_DIR.glob(\"*.md\")):\n",
|
||||
" print(\"•\", p.name)\n",
|
||||
"if SCARICA_ZIP:\n",
|
||||
" files.download(str(archive))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell-011",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Se qualcosa non funziona\n",
|
||||
"- **GPU assente:** seleziona un runtime GPU e riesegui tutto. Se Colab non offre una T4, attendi o usa la GPU disponibile.\n",
|
||||
"- **Memoria GPU insufficiente:** imposta `PRECISIONE = \"int8_float16\"`; se necessario scegli `MODELLO = \"turbo\"`, disconnetti/elimina il runtime e riparti.\n",
|
||||
"- **Download del modello interrotto:** riprova la cella 5 quando la connessione è disponibile.\n",
|
||||
"- **Errore CUDA/cuDNN:** riparti da un runtime nuovo e riesegui le celle nell'ordine. Il log è incluso nello ZIP.\n",
|
||||
"- **Audio lungo:** usa Google Drive, attiva `SALVA_SU_DRIVE` e scarica i risultati prima che la sessione termini. I file di lavoro in `/content` sono temporanei; la copia su Drive viene effettuata nella cella 6.\n",
|
||||
"- **Passaggi strani, sovrapposti o nomi sbagliati:** riascolta i punti indicati. Il modello non può certificare da solo l'incomprensibilità né garantire una trascrizione integrale fedele.\n",
|
||||
"\n",
|
||||
"### Riferimenti\n",
|
||||
"- [faster-whisper: installazione, GPU e trascrizione](https://github.com/SYSTRAN/faster-whisper)\n",
|
||||
"- [FAQ ufficiali di Google Colab](https://research.google.com/colaboratory/faq.html)\n",
|
||||
"\n",
|
||||
"Notebook verificato staticamente e funzioni di esportazione provate con dati sintetici; l'esecuzione end-to-end su una T4 Colab va verificata nel proprio runtime.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"accelerator": "GPU",
|
||||
"colab": {
|
||||
"name": "Trascrizione_MP3_GPU_T4.ipynb",
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.13"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
Reference in new issue
Block a user