Switch transcription backend from Whisper to Mistral AI API

This commit is contained in:
Damien
2026-06-10 16:36:32 +02:00
parent bda2b516a4
commit ba63761c90
4 changed files with 374 additions and 507 deletions

View File

@@ -1,6 +1,6 @@
# Audio Summary with Local LLM # Audio Summary with Local LLM
This tool is designed to provide a quick and concise summary of audio and video files. It supports summarizing content either from a local file or directly from YouTube. The tool uses Whisper for transcription and a local version of Llama3 (via Ollama) for generating summaries. This tool is designed to provide a quick and concise summary of audio and video files. It supports summarizing content either from a local file or directly from YouTube. The tool uses Mistral AI API for transcription and a local version of Llama3 (via Ollama) for generating summaries.
> [!TIP] > [!TIP]
> It is possible to change the model you wish to use. > It is possible to change the model you wish to use.
@@ -10,7 +10,7 @@ This tool is designed to provide a quick and concise summary of audio and video
- **YouTube Integration**: Download and summarize content directly from YouTube. - **YouTube Integration**: Download and summarize content directly from YouTube.
- **Local File Support**: Summarize audio/video files available on your local disk. - **Local File Support**: Summarize audio/video files available on your local disk.
- **Transcription**: Converts audio content to text using Whisper. - **Transcription**: Converts audio content to text using Mistral AI API.
- **Summarization**: Generates a concise summary using Llama3 (Ollama). - **Summarization**: Generates a concise summary using Llama3 (Ollama).
- **Transcript Only Option**: Option to only transcribe the audio content without generating a summary. - **Transcript Only Option**: Option to only transcribe the audio content without generating a summary.
- **Device Optimization**: Automatically uses the best available hardware (MPS for Mac, CUDA for NVIDIA GPUs, or CPU). - **Device Optimization**: Automatically uses the best available hardware (MPS for Mac, CUDA for NVIDIA GPUs, or CPU).
@@ -23,6 +23,8 @@ Before you start using this tool, you need to install the following dependencies
- [Ollama](https://ollama.com) for LLM model management - [Ollama](https://ollama.com) for LLM model management
- `ffmpeg` (required for audio processing) - `ffmpeg` (required for audio processing)
- [uv](https://docs.astral.sh/uv/getting-started/installation/) for package management - [uv](https://docs.astral.sh/uv/getting-started/installation/) for package management
- Mistral AI API key (free tier available)
- Mistral AI API key (set as `MISTRAL_API_KEY` environment variable)
## Installation ## Installation
@@ -39,6 +41,17 @@ uv sync
source .venv/bin/activate # On Windows: .venv\Scripts\activate source .venv/bin/activate # On Windows: .venv\Scripts\activate
``` ```
### Mistral API Setup
1. Sign up for a Mistral AI account and get your API key from the [Mistral AI platform](https://mistral.ai/)
2. Set your API key as an environment variable:
```bash
export MISTRAL_API_KEY="your-api-key-here"
```
Or add it to your `.bashrc` or `.zshrc` file for persistent use.
### LLM Requirement ### LLM Requirement
[Download and install](https://ollama.com) Ollama to carry out LLM Management. More details about LLM models supported can be found on the Ollama [GitHub](https://github.com/ollama/ollama). [Download and install](https://ollama.com) Ollama to carry out LLM Management. More details about LLM models supported can be found on the Ollama [GitHub](https://github.com/ollama/ollama).
@@ -196,24 +209,21 @@ OLLAMA_MODEL = "llama3"
WHISPER_MODEL = "openai/whisper-large-v2" WHISPER_MODEL = "openai/whisper-large-v2"
``` ```
#### Changing the Whisper Model #### Changing the Mistral Transcription Model
To use a different Whisper model for transcription: To use a different Mistral model for transcription:
1. Update the `WHISPER_MODEL` variable with one of these options: 1. Update the `MISTRAL_MODEL` variable with one of the available Mistral audio models:
- `"openai/whisper-tiny"` (fastest, least accurate) - `"voxtral-mini-latest"` (default, good balance)
- `"openai/whisper-base"` (faster, less accurate) - `"voxtral-small-latest"` (smaller, faster)
- `"openai/whisper-small"` (balanced)
- `"openai/whisper-medium"` (slower, more accurate)
- `"openai/whisper-large-v2"` (slowest, most accurate)
2. Example: 2. Example:
```python ```python
WHISPER_MODEL = "openai/whisper-medium" # A good balance between speed and accuracy MISTRAL_MODEL = "voxtral-small-latest" # Faster transcription
``` ```
For CPU-only systems, using a smaller model like `whisper-base` is recommended for better performance. The Mistral API handles all hardware acceleration automatically, so no device selection is needed.
#### Changing the LLM Model #### Changing the LLM Model

View File

@@ -14,12 +14,11 @@ authors = [
] ]
dependencies = [ dependencies = [
"ffmpeg>=1.4", "ffmpeg>=1.4",
"mistralai>=0.1.0",
"ollama>=0.4.7", "ollama>=0.4.7",
"openai-whisper>=20240930",
"torch>=2.6.0", "torch>=2.6.0",
"torchaudio>=2.6.0", "torchaudio>=2.6.0",
"torchvision>=0.21.0", "torchvision>=0.21.0",
"transformers>=4.50.2",
"yt-dlp>=2025.3.27", "yt-dlp>=2025.3.27",
] ]
[project.scripts] [project.scripts]

View File

@@ -1,88 +1,85 @@
import ollama
import argparse import argparse
import os
from pathlib import Path from pathlib import Path
from transformers import pipeline
import ollama
import yt_dlp import yt_dlp
import torch from mistralai.client import Mistral
from mistralai.client.models import File
OLLAMA_MODEL = "llama3" OLLAMA_MODEL = "llama3"
WHISPER_MODEL = "openai/whisper-large-v2" MISTRAL_MODEL = "voxtral-mini-latest" # Mistral audio transcription model
WHISPER_LANGUAGE = "en" # Set to desired language or None for auto-detection MISTRAL_LANGUAGE = "en" # Set to desired language or None for auto-detection
# Function to download a video from YouTube using yt-dlp # Function to download a video from YouTube using yt-dlp
def download_from_youtube(url: str, path: str): def download_from_youtube(url: str, path: str):
ydl_opts = { ydl_opts = {
'format': 'bestaudio/best', "format": "bestaudio/best",
'outtmpl': str(Path(path) / 'to_transcribe.%(ext)s'), "outtmpl": str(Path(path) / "to_transcribe.%(ext)s"),
'postprocessors': [{ "postprocessors": [
'key': 'FFmpegExtractAudio', {
'preferredcodec': 'mp3', "key": "FFmpegExtractAudio",
'preferredquality': '192', "preferredcodec": "mp3",
}], "preferredquality": "192",
}
],
} }
with yt_dlp.YoutubeDL(ydl_opts) as ydl: with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url]) ydl.download([url])
# Function to get the best available device
def get_device():
if torch.backends.mps.is_available():
return "mps"
elif torch.cuda.is_available():
return "cuda"
else:
return "cpu"
# Function to transcribe an audio file using the transformers pipeline # Function to get Mistral API key from environment variables
def transcribe_file(file_path: str, output_file: str, language: str = None) -> str: def get_mistral_api_key():
# Get the best available device api_key = os.getenv("MISTRAL_API_KEY")
device = get_device() if not api_key:
print(f"Using device: {device} for transcription") raise ValueError(
"MISTRAL_API_KEY environment variable not set. Please set your Mistral API key."
# Load the pipeline model for automatic speech recognition
transcriber = pipeline(
"automatic-speech-recognition",
model=WHISPER_MODEL,
device=device,
chunk_length_s=30, # Process in 30-second chunks
return_timestamps=True # Enable timestamp generation for longer audio
) )
return api_key
# Transcribe the audio file
# For CPU, we might want to use a smaller model or chunk the audio if memory is an issue
if device == "cpu":
print("Warning: Using CPU for transcription. This may be slow.")
# Set up generation keyword arguments including language # Function to transcribe an audio file using Mistral API
generate_kwargs = {} def transcribe_file(file_path: str, output_file: str, language: str = None) -> str:
if language and language.lower() != "auto": # Get Mistral API key
generate_kwargs["language"] = language api_key = get_mistral_api_key()
print(f"Transcribing in language: {language}")
# Set up language parameter
transcribe_language = language if language and language.lower() != "auto" else None
if transcribe_language:
print(f"Transcribing in language: {transcribe_language}")
else: else:
print("Using automatic language detection") print("Using automatic language detection")
# Transcribe the audio file # Initialize Mistral client
print("Starting transcription (this may take a while for longer files)...") print("Starting transcription with Mistral API...")
transcribe = transcriber(file_path, generate_kwargs=generate_kwargs) # Open the audio file and send to Mistral API
with open(file_path, "rb") as audio_file:
# Create File object as expected by Mistral API
file_obj = File(content=audio_file, file_name=Path(file_path).name)
# Extract the full text from the chunked transcription # Use async transcription for better performance
if isinstance(transcribe, dict) and "text" in transcribe: import asyncio
# Simple case - just one chunk
full_text = transcribe["text"] async def transcribe_async():
elif isinstance(transcribe, dict) and "chunks" in transcribe: with Mistral(api_key=api_key) as mistral:
# Multiple chunks with timestamps response = await mistral.audio.transcriptions.complete_async(
full_text = " ".join([chunk["text"] for chunk in transcribe["chunks"]]) model=MISTRAL_MODEL, file=file_obj, language=transcribe_language
else: )
# Fallback for other return formats return response.text
full_text = transcribe["text"] if "text" in transcribe else str(transcribe)
# Run the async transcription
full_text = asyncio.run(transcribe_async())
# Save the transcribed text to the specified temporary file # Save the transcribed text to the specified temporary file
with open(output_file, 'w') as tmp_file: with open(output_file, "w") as tmp_file:
tmp_file.write(full_text) tmp_file.write(full_text)
print(f"Transcription saved to file: {output_file}") print(f"Transcription saved to file: {output_file}")
# Return the transcribed text # Return the transcribed text
return full_text return full_text
# Function to summarize a text using the Ollama model # Function to summarize a text using the Ollama model
def summarize_text(text: str, output_path: str) -> str: def summarize_text(text: str, output_path: str) -> str:
# Define the system prompt for the Ollama model # Define the system prompt for the Ollama model
@@ -112,20 +109,33 @@ def summarize_text(text: str, output_path: str) -> str:
# Print the generated summary # Print the generated summary
return response["message"]["content"] return response["message"]["content"]
def main(): def main():
# Parse command line arguments # Parse command line arguments
parser = argparse.ArgumentParser(description="Download, transcribe, and summarize audio or video files.") parser = argparse.ArgumentParser(
description="Download, transcribe, and summarize audio or video files."
)
group = parser.add_mutually_exclusive_group(required=True) group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--from-youtube", type=str, help="YouTube URL to download.") group.add_argument("--from-youtube", type=str, help="YouTube URL to download.")
group.add_argument("--from-local", type=str, help="Path to the local audio file.") group.add_argument("--from-local", type=str, help="Path to the local audio file.")
parser.add_argument("--output", type=str, default="./summary.md", help="Output markdown file path.") parser.add_argument(
parser.add_argument("--transcript-only", action='store_true', help="Only transcribe the file, do not summarize.") "--output", type=str, default="./summary.md", help="Output markdown file path."
parser.add_argument("--language", type=str, help="Language code for transcription (e.g., 'en', 'fr', 'es', or 'auto' for detection)") )
parser.add_argument(
"--transcript-only",
action="store_true",
help="Only transcribe the file, do not summarize.",
)
parser.add_argument(
"--language",
type=str,
help="Language code for transcription (e.g., 'en', 'fr', 'es', or 'auto' for detection)",
)
args = parser.parse_args() args = parser.parse_args()
# Determine language setting # Determine language setting
language = args.language if args.language else WHISPER_LANGUAGE language = args.language if args.language else MISTRAL_LANGUAGE
if language and language.lower() == "auto": if language and language.lower() == "auto":
language = None # None triggers automatic language detection language = None # None triggers automatic language detection
@@ -147,7 +157,9 @@ def main():
print(f"Transcribing file: {file_path}") print(f"Transcribing file: {file_path}")
# Transcribe the audio file # Transcribe the audio file
transcript = transcribe_file(str(file_path), data_directory / "transcript.txt", language) transcript = transcribe_file(
str(file_path), data_directory / "transcript.txt", language
)
if args.transcript_only: if args.transcript_only:
print("Transcription complete. Skipping summary generation.") print("Transcription complete. Skipping summary generation.")
@@ -163,5 +175,6 @@ def main():
md_file.write(summary) md_file.write(summary)
print(f"Summary written to {args.output}") print(f"Summary written to {args.output}")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

677
uv.lock generated

File diff suppressed because it is too large Load Diff