Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Git
.git
.gitignore

# Python
.python-version
__pycache__/
*.py[cod]
.venv/
.env

# Docker
Dockerfile

# Other
.ropeproject
25 changes: 25 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Docker Build CI

on:
push:
branches-ignore:
- 'main'

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: false
load: true
tags: agent-utils:build-test
30 changes: 30 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Docker Image CI

on:
push:
branches: [ "main" ]

jobs:
build-and-publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Log in to the GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latest
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.10
30 changes: 30 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Use an official Python runtime as a parent image
FROM python:3.10-slim

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Install system dependencies (ffmpeg for pydub)
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*

# Set the working directory in the container
WORKDIR /app

# Install uv
RUN pip install uv

# Copy the dependency files
COPY pyproject.toml uv.lock ./

# Install dependencies using uv
RUN uv export --no-dev | uv pip install --system -r -

# Copy the application code
COPY ./app ./app

# Expose the port the app runs on
EXPOSE 8000

# Run the application
CMD ["fastapi", "run", "--host", "0.0.0.0", "--port", "8000"]
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,35 @@
# agent-utils

Various utility APIs for agents

## Usage

Run directly:

```sh
uv run fastapi [dev|run]
```

Run the API in development or poduction mode.

Run production mode in a container:

```sh
docker run -p 8000:8000 --env-file .env agent-utils
```

## Functions

Find the API documentation at http://…:8000/docs.

### Transcribe audio

Post the URL of an audio file and get a text transcript returned. Currently works with MP3.

## Configuration

The app needs an OpenAI API key to work, in the `.env` file.

```toml
OPENAI_API_KEY=… # Open AI, API key for access to the Whisper API
```
1 change: 1 addition & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import mp3transcribe
38 changes: 38 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from typing import Union

from fastapi import FastAPI, HTTPException
from dotenv import load_dotenv

from .mp3transcribe import Mp3Transcribe, AudioFile

load_dotenv()
app = FastAPI()


@app.get("/")
def read_root():
return {"Hello": "World"}



@app.post("/mp3transcribe/")
def transcribe_mp3(audio_file: AudioFile):
"""
Transcribes an MP3 audio file.

This endpoint accepts an MP3 audio file, downloads it, and transcribes the audio content to text.

Args:
audio_file (mp3transcribe.AudioFile): An object containing the URL and language of the MP3 audio file to be transcribed.

Returns:
dict: A dictionary containing the transcribed text.

Raises:
HTTPException: If the audio file cannot be downloaded, an HTTP 500 error is raised.
"""
mp3 = Mp3Transcribe(audio_file.url, audio_file.lang)
if not mp3.download():
raise HTTPException(status_code=500, detail="Failed to download audio file")
transcript = mp3.transcribe()
return {"text": transcript}
81 changes: 81 additions & 0 deletions app/mp3transcribe/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import logging
from pathlib import Path
from tempfile import TemporaryDirectory

from pydantic import BaseModel
import requests
from openai import OpenAI
from pydub import AudioSegment



class AudioFile(BaseModel):
url: str
lang: str = 'en'


class Mp3Transcribe:
"""
A class to handle downloading and transcribing MP3 audio files using OpenAI's transcription service.

Attributes:
url (str): The URL of the MP3 file to download.
lang (str): The language of the audio for transcription. Default is 'en'.
client (OpenAI): An instance of the OpenAI client.
temp_dir (TemporaryDirectory): A temporary directory to store downloaded and processed files.
audio (Path): The path to the downloaded MP3 file.
transcript (Path): The path to the generated transcript file.

Methods:
download() -> bool:
Downloads the MP3 file from the specified URL and saves it to a temporary directory.
Returns True if the download is successful, otherwise False.

transcribe() -> str:
Transcribes the downloaded MP3 file in 10-minute segments using OpenAI's transcription service.
Saves the transcript to a text file in the temporary directory.
Returns the transcribed text.
"""
def __init__(self, url, lang='en'):
self.url = url
self.lang = lang
self.client = OpenAI()
self.temp_dir = TemporaryDirectory()

def download(self) -> bool:
self.audio = Path(self.temp_dir.name).joinpath('audio.mp3')
logging.debug(f'Downloading {self.url} to {self.audio}')
response = requests.get(self.url)
if response.status_code != 200:
logging.error(f'Failed to download {self.url}, response {response}')
return False
with open(self.audio, 'wb') as f:
f.write(response.content)
logging.debug(f'Downloaded {self.url} to {self.audio}')
return True

def transcribe(self):
audio_file = AudioSegment.from_mp3(self.audio)
ten_minutes = 10 * 60 * 1000
audio_10s = audio_file[::ten_minutes]
self.transcript = Path(self.temp_dir.name).joinpath('transcript.txt')
with open(self.transcript, 'w') as t:
i = 0
for audio in audio_10s:
logging.debug(f'Transcribing segment {i}')
seg10_name = Path(self.temp_dir.name).joinpath(f'd10-{i}.mp3')
audio.export(seg10_name, format="mp3")
with open(seg10_name, 'rb') as f:
text = self.client.audio.transcriptions.create(
language=self.lang,
file=f,
model="whisper-1",
#prompt=prompt
response_format="text"
)
#prompt = transcript
t.write(text)
i += 1
logging.debug(f'Transcribed {self.audio} to {self.transcript}')
return self.transcript.read_text()

15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[project]
name = "agent-utils"
version = "0.1.0"
classifiers = ["Private :: Do Not Upload"]
description = "Utility APIs for agents"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"fastapi[standard]>=0.115.5",
"openai>=1.55.3",
"pydantic-settings>=2.6.1",
"pydub>=0.25.1",
"python-dotenv>=1.0.1",
"requests>=2.32.3",
]
Loading