From 269d192b12380fd082f10e7ba69fb5f9bfd61954 Mon Sep 17 00:00:00 2001 From: MRD2F Date: Sun, 23 Nov 2025 12:00:10 +0100 Subject: [PATCH 1/8] Created structure of app flask and ConvertorService class --- pyproject.toml | 1 + requirements.txt | 5 ++++ .../service/convertor_controller.py} | 0 .../convertor/service/convertor_service.py | 24 +++++++++++++++++++ src/app/main.py | 9 +++++++ src/models/convertor.py | 0 6 files changed, 39 insertions(+) rename src/{models/__init__.py => app/convertor/service/convertor_controller.py} (100%) create mode 100644 src/app/convertor/service/convertor_service.py create mode 100644 src/app/main.py delete mode 100644 src/models/convertor.py diff --git a/pyproject.toml b/pyproject.toml index b2e4893..13fdd9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "ffmpeg>=1.4", + "flask>=3.1.2", "openai-whisper>=20250625", "pandas>=2.3.3", "torch>=2.9.1", diff --git a/requirements.txt b/requirements.txt index cfcc747..be6a3af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,23 @@ Package Version ------------------------ ----------- asttokens 3.0.1 +blinker 1.9.0 certifi 2025.11.12 charset-normalizer 3.4.4 +click 8.3.1 comm 0.2.3 debugpy 1.8.17 decorator 5.2.1 executing 2.2.1 ffmpeg 1.4 filelock 3.20.0 +flask 3.1.2 fsspec 2025.10.0 idna 3.11 ipykernel 7.1.0 ipython 9.7.0 ipython-pygments-lexers 1.1.1 +itsdangerous 2.2.0 jedi 0.19.2 jinja2 3.1.6 jupyter-client 8.6.3 @@ -72,3 +76,4 @@ typing-extensions 4.15.0 tzdata 2025.2 urllib3 2.5.0 wcwidth 0.2.14 +werkzeug 3.1.3 diff --git a/src/models/__init__.py b/src/app/convertor/service/convertor_controller.py similarity index 100% rename from src/models/__init__.py rename to src/app/convertor/service/convertor_controller.py diff --git a/src/app/convertor/service/convertor_service.py b/src/app/convertor/service/convertor_service.py new file mode 100644 index 0000000..c50d03d --- /dev/null +++ b/src/app/convertor/service/convertor_service.py @@ -0,0 +1,24 @@ +#import whisper + +class ConvertorService: + quality = "tiny" + + # # Load the Whisper model (choose: tiny, base, small, medium, large) + # model = whisper.load_model(quality) + + + # print("Transcribing... this may take a few minutes for a 1h file.") + + # result = model.transcribe(audio_path) + + # # Print text + # print(result["text"]) + + # # Save transcript to file + # with open(f"{output_file_name}.txt", "w", encoding="utf-8") as f: + # f.write(result["text"]) + @classmethod + def create_text(cls, nome): + pippo = "Ciao" + cls.quality + + return f"{pippo} {nome}" diff --git a/src/app/main.py b/src/app/main.py new file mode 100644 index 0000000..25e15f0 --- /dev/null +++ b/src/app/main.py @@ -0,0 +1,9 @@ +from flask import Flask, request +from convertor.service.convertor_service import ConvertorService + +app = Flask(__name__) +@app.route("/") +def hello_world(): + name = request.args.get('name') + text = ConvertorService.create_text(name) + return f"

{text}

" \ No newline at end of file diff --git a/src/models/convertor.py b/src/models/convertor.py deleted file mode 100644 index e69de29..0000000 From cae05ec0b542c14839b85d82210a27296298d68b Mon Sep 17 00:00:00 2001 From: MRD2F Date: Mon, 24 Nov 2025 19:38:12 +0100 Subject: [PATCH 2/8] Initial structure and features for the transcription class --- .../app/convertor/service}/__init__.py | 0 src/app/convertor/service/data/__init__.py | 0 .../data/inputs/5846093734223028963.ogg | Bin 0 -> 32303 bytes .../data/inputs/5846093734223028963.ogx | Bin 0 -> 32303 bytes src/app/convertor/service/transcription.py | 103 ++++++++++++++++++ .../service/transcription_service.py | 18 +++ uv.lock | 61 +++++++++++ 7 files changed, 182 insertions(+) rename {data => src/app/convertor/service}/__init__.py (100%) create mode 100644 src/app/convertor/service/data/__init__.py create mode 100644 src/app/convertor/service/data/inputs/5846093734223028963.ogg create mode 100644 src/app/convertor/service/data/inputs/5846093734223028963.ogx create mode 100644 src/app/convertor/service/transcription.py create mode 100644 src/app/convertor/service/transcription_service.py diff --git a/data/__init__.py b/src/app/convertor/service/__init__.py similarity index 100% rename from data/__init__.py rename to src/app/convertor/service/__init__.py diff --git a/src/app/convertor/service/data/__init__.py b/src/app/convertor/service/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/convertor/service/data/inputs/5846093734223028963.ogg b/src/app/convertor/service/data/inputs/5846093734223028963.ogg new file mode 100644 index 0000000000000000000000000000000000000000..6e1cc6fd5e59203b7e28a22fbdde565fc14a0d5a GIT binary patch literal 32303 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 literal 0 HcmV?d00001 diff --git a/src/app/convertor/service/data/inputs/5846093734223028963.ogx b/src/app/convertor/service/data/inputs/5846093734223028963.ogx new file mode 100644 index 0000000000000000000000000000000000000000..6e1cc6fd5e59203b7e28a22fbdde565fc14a0d5a GIT binary patch literal 32303 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 literal 0 HcmV?d00001 diff --git a/src/app/convertor/service/transcription.py b/src/app/convertor/service/transcription.py new file mode 100644 index 0000000..9b62ce1 --- /dev/null +++ b/src/app/convertor/service/transcription.py @@ -0,0 +1,103 @@ +import whisper + +class Transctiption: + def __init__(self, model_id, input_file_name="", show_text=False, + output_file_name="", text_preview_size=None, + language="english"): + self.model_id = model_id + self.input_file_name = input_file_name + self.show_text = show_text + self.output_file_name = output_file_name + self.text_preview_size = text_preview_size + self.language = language + + ########## Sanity checks for whisper model use ######### + self.whisper_allowed_extensions = ['flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg', 'wav', 'webm'] + self.whisper_model_ids = ["tiny", "base", "small", "medium", "large", "turbo"] + + # The default setting (which selects the turbo model) works well for transcribing English. + # However, the turbo model is not trained for translation tasks. + # If you need to translate non-English speech into English, use one of the + # multilingual models (tiny, base, small, medium, large) instead of turbo. + + self.whisper_model_ids_english_only = ["tiny.en", "base.en", "small.en", "medium.en"] + self._check_file_extension() + self._check_whisper_model_id() + + def _check_file_extension(self): + ext = self.input_file_name.rsplit(".", 1)[-1].lower() + + if ext not in self.whisper_allowed_extensions: + allowed = ", ".join(self.whisper_allowed_extensions) + raise ValueError( + f"Invalid file format: .{ext}\n" + f"Allowed formats are: {allowed}" + ) + + def _check_whisper_model_id(self): + if self.model_id not in self.whisper_model_ids: + allowed = ", ".join(self.whisper_model_ids) + raise ValueError( + f"Invalid model ID selection: {self.model_id}\n" + f"Allowed formats are: {allowed}" + ) + + def _get_model(self): + model = whisper.load_model(self.model_id) + return model + + def save_transcription(self, text, output_file_name=""): + file_name = self.output_file_name if not output_file_name else output_file_name + with open(f"{file_name}.txt", "w", encoding="utf-8") as f: + f.write(text) + + + def get_transcription(self): + #, input_file_name = "", show_text=False, output_file_name=""): + model = self._get_model() + print(f"Using as requested model {self.model_id}.") + + + print(f"Transcribing file {self.input_file_name}... this may take a few minutes depening of the file size.") + # load audio and pad/trim it to fit 30 seconds + # audio = whisper.load_audio("audio.mp3") + # audio = whisper.pad_or_trim(audio) + + # detect the spoken language + # _, probs = model.detect_language(mel) + # print(f"Detected language: {max(probs, key=probs.get)}") + + + result = model.transcribe(self.input_file_name, ) + + if self.show_text | show_text: + if self.text_preview_size: + print(result['text'][:self.text_preview_size]) + else: + print(result["text"]) + + if (len(self.output_file_name) > 0) |(len(self.output_file_name) > 0): + file_name = self.output_file_name if not output_file_name else output_file_name + self.save_transcription(result["text"], file_name) + print(f"Saved transcription as: {file_name}.") + + +if __name__ == "__main__": + + data_dir = "data" + input_file_name = f"{data_dir}/inputs/5846093734223028963.ogg" + output_file_name = f"{data_dir}/outputs/5846093734223028963" + + + model_id = "tiny" + show_text = True + text_preview_size = 10 + + transctiption_service = Transctiption(model_id=model_id, input_file_name=input_file_name, + show_text=show_text, output_file_name=output_file_name, + text_preview_size=text_preview_size) + + transctiption_service.get_transcription() + + + diff --git a/src/app/convertor/service/transcription_service.py b/src/app/convertor/service/transcription_service.py new file mode 100644 index 0000000..140e360 --- /dev/null +++ b/src/app/convertor/service/transcription_service.py @@ -0,0 +1,18 @@ +from transcription import Transctiption + +class TransctiptionService: + data_dir = "data" + input_file_name = f"{data_dir}/inputs/5846093734223028963.ogg" + output_file_name = f"{data_dir}/outputs/5846093734223028963" + model_id = "tiny" + show_text = True + text_preview_size = 10 + + transctiption = Transctiption(model_id=model_id, input_file_name=input_file_name, + show_text=show_text, output_file_name=output_file_name, + text_preview_size=text_preview_size) + + #transctiption.get_transcription() + + + diff --git a/uv.lock b/uv.lock index d334310..429fb70 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.12" +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -68,6 +77,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -92,6 +113,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, ] +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + [[package]] name = "fsspec" version = "2025.10.0" @@ -110,6 +148,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -649,6 +696,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "ffmpeg" }, + { name = "flask" }, { name = "openai-whisper" }, { name = "pandas" }, { name = "torch" }, @@ -657,6 +705,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "ffmpeg", specifier = ">=1.4" }, + { name = "flask", specifier = ">=3.1.2" }, { name = "openai-whisper", specifier = ">=20250625" }, { name = "pandas", specifier = ">=2.3.3" }, { name = "torch", specifier = ">=2.9.1" }, @@ -823,3 +872,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599 wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, +] From c0ca1610d9f7ffb5973f2d4a9b18f8456986607f Mon Sep 17 00:00:00 2001 From: MRD2F Date: Mon, 24 Nov 2025 19:45:59 +0100 Subject: [PATCH 3/8] Updated README.md with instructions on how to test the transcriptor class --- README.md | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e4c31a5..876092f 100644 --- a/README.md +++ b/README.md @@ -1 +1,117 @@ -# Talk2Text \ No newline at end of file +# ๐ŸŽง Audio Transcription Service + +A Python application that provides audio transcription using Whisper and +OpenAI models.\ +The project is managed with **uv** (Ultrafast Python Package Manager). + +------------------------------------------------------------------------ + +## ๐Ÿš€ 1. Installation + +### **1.1. Clone the repository** + +``` bash +git clone https://github.com/yourusername/yourrepo.git +cd yourrepo +``` + +### **1.2. Install uv (if not already installed)** + +``` bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +---or via pip--- + +``` bash +pip install uv +``` + +### **1.3. Create and sync the environment** + +uv uses `pyproject.toml` + `uv.lock` to reproduce the environment: + +``` bash +uv sync +``` + +This automatically: + +- Creates a virtual environment (`.venv`) +- Installs all dependencies +- Pins exact versions from `uv.lock` + +------------------------------------------------------------------------ + +## ๐ŸŽค 2. Project Structure (relevant section) + + src/ + โ””โ”€โ”€ app/ + โ””โ”€โ”€ convertor/ + โ””โ”€โ”€ service/ + โ””โ”€โ”€ transcription_service.py + data/ + โ””โ”€โ”€ inputs/ + โ””โ”€โ”€ file.ogg + +------------------------------------------------------------------------ + +## ๐Ÿƒ 3. Running the Transcription Script + +Run from the **root directory**: + +``` bash +uv run src/app/convertor/service/transcription_service.py +``` + +### Important + +Running from the project root ensures that relative paths like +`data/inputs/...` resolve correctly. + + +## ๐Ÿงช 5. Running Tests (if applicable) + +``` bash +uv run pytest +``` + +------------------------------------------------------------------------ + +## ๐Ÿ›  6. Updating Dependencies + +### Add a new package + +``` bash +uv add +``` + +### Upgrade all dependencies + +``` bash +uv lock --upgrade +uv sync +``` + +------------------------------------------------------------------------ + +## โ— Troubleshooting + +### **FileNotFoundError for audio inputs** + +Ensure the script is always run from the **project root**. + +Correct: + +``` bash +uv run src/app/convertor/service/transcription_service.py +``` + +Incorrect: + +``` bash +cd src/app/convertor/service/ +uv run transcription_service.py # โŒ breaks relative paths +``` + +------------------------------------------------------------------------ From 8c312520b6e499c1391dbd6bf7c2118e4a18b422 Mon Sep 17 00:00:00 2001 From: MRD2F Date: Tue, 25 Nov 2025 23:01:08 +0100 Subject: [PATCH 4/8] Created pytests for the transcription class, fixes on its class, work on the main script with flask --- .github/ci.yml | 39 ++++++++++++++ pyproject.toml | 1 + .../convertor/service/convertor_service.py | 36 ++++++------- src/app/convertor/service/transcription.py | 37 +++++++------ .../service/transcription_service.py | 18 ------- src/app/main.py | 3 +- tests/__init__.py | 0 tests/test_core.py | 45 ++++++++++++++++ uv.lock | 54 +++++++++++++++++++ 9 files changed, 177 insertions(+), 56 deletions(-) create mode 100644 .github/ci.yml delete mode 100644 src/app/convertor/service/transcription_service.py create mode 100644 tests/__init__.py diff --git a/.github/ci.yml b/.github/ci.yml new file mode 100644 index 0000000..5216a63 --- /dev/null +++ b/.github/ci.yml @@ -0,0 +1,39 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python application + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 13fdd9b..7b18714 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,5 +9,6 @@ dependencies = [ "flask>=3.1.2", "openai-whisper>=20250625", "pandas>=2.3.3", + "pytest==8.4.2", "torch>=2.9.1", ] diff --git a/src/app/convertor/service/convertor_service.py b/src/app/convertor/service/convertor_service.py index c50d03d..09a8256 100644 --- a/src/app/convertor/service/convertor_service.py +++ b/src/app/convertor/service/convertor_service.py @@ -1,24 +1,18 @@ -#import whisper +from convertor.service.transcription import Transcription -class ConvertorService: - quality = "tiny" - - # # Load the Whisper model (choose: tiny, base, small, medium, large) - # model = whisper.load_model(quality) - - - # print("Transcribing... this may take a few minutes for a 1h file.") - # result = model.transcribe(audio_path) - - # # Print text - # print(result["text"]) - - # # Save transcript to file - # with open(f"{output_file_name}.txt", "w", encoding="utf-8") as f: - # f.write(result["text"]) +class ConvertorService: + @classmethod - def create_text(cls, nome): - pippo = "Ciao" + cls.quality - - return f"{pippo} {nome}" + def create_text(cls): + #data_dir = "data" + input_file_name = "./convertor/service/data/inputs/5846093734223028963.ogg" + #output_file_name = "./data/outputs/5846093734223028963" + model_id = "tiny" + show_text = True + text_preview_size = 10 + + transcription = Transcription(model_id=model_id, input_file_name=input_file_name, + show_text=show_text,text_preview_size=text_preview_size) + + return transcription.get_transcription() diff --git a/src/app/convertor/service/transcription.py b/src/app/convertor/service/transcription.py index 9b62ce1..df5cc79 100644 --- a/src/app/convertor/service/transcription.py +++ b/src/app/convertor/service/transcription.py @@ -1,11 +1,12 @@ import whisper +import os -class Transctiption: - def __init__(self, model_id, input_file_name="", show_text=False, +class Transcription: + def __init__(self, model_id="tiny", input_file_name=".ogg", show_text=False, output_file_name="", text_preview_size=None, language="english"): self.model_id = model_id - self.input_file_name = input_file_name + self.input_file_name = input_file_name #self.load_file(input_file_name) self.show_text = show_text self.output_file_name = output_file_name self.text_preview_size = text_preview_size @@ -24,6 +25,13 @@ def __init__(self, model_id, input_file_name="", show_text=False, self._check_file_extension() self._check_whisper_model_id() + + @staticmethod + def load_file(input_file_name): + if not os.path.exists(input_file_name): + raise FileNotFoundError(f"File: {input_file_name} does not exist") + return input_file_name + def _check_file_extension(self): ext = self.input_file_name.rsplit(".", 1)[-1].lower() @@ -33,6 +41,7 @@ def _check_file_extension(self): f"Invalid file format: .{ext}\n" f"Allowed formats are: {allowed}" ) + return True def _check_whisper_model_id(self): if self.model_id not in self.whisper_model_ids: @@ -41,7 +50,8 @@ def _check_whisper_model_id(self): f"Invalid model ID selection: {self.model_id}\n" f"Allowed formats are: {allowed}" ) - + return True + def _get_model(self): model = whisper.load_model(self.model_id) return model @@ -53,11 +63,8 @@ def save_transcription(self, text, output_file_name=""): def get_transcription(self): - #, input_file_name = "", show_text=False, output_file_name=""): model = self._get_model() print(f"Using as requested model {self.model_id}.") - - print(f"Transcribing file {self.input_file_name}... this may take a few minutes depening of the file size.") # load audio and pad/trim it to fit 30 seconds # audio = whisper.load_audio("audio.mp3") @@ -67,37 +74,37 @@ def get_transcription(self): # _, probs = model.detect_language(mel) # print(f"Detected language: {max(probs, key=probs.get)}") - result = model.transcribe(self.input_file_name, ) - if self.show_text | show_text: + if self.show_text: if self.text_preview_size: print(result['text'][:self.text_preview_size]) else: print(result["text"]) - if (len(self.output_file_name) > 0) |(len(self.output_file_name) > 0): + if (len(self.output_file_name) > 0) or (len(self.output_file_name) > 0): file_name = self.output_file_name if not output_file_name else output_file_name self.save_transcription(result["text"], file_name) print(f"Saved transcription as: {file_name}.") + + return result["text"] if __name__ == "__main__": data_dir = "data" - input_file_name = f"{data_dir}/inputs/5846093734223028963.ogg" - output_file_name = f"{data_dir}/outputs/5846093734223028963" - + input_file_name = "./src/app/convertor/service/data/inputs/5846093734223028963.ogg" + output_file_name = "./src/app/convertor/service/data/inputs/5846093734223028963.ogx" model_id = "tiny" show_text = True text_preview_size = 10 - transctiption_service = Transctiption(model_id=model_id, input_file_name=input_file_name, + transcription_service = Transcription(model_id=model_id, input_file_name=input_file_name, show_text=show_text, output_file_name=output_file_name, text_preview_size=text_preview_size) - transctiption_service.get_transcription() + transcription_service.get_transcription() diff --git a/src/app/convertor/service/transcription_service.py b/src/app/convertor/service/transcription_service.py deleted file mode 100644 index 140e360..0000000 --- a/src/app/convertor/service/transcription_service.py +++ /dev/null @@ -1,18 +0,0 @@ -from transcription import Transctiption - -class TransctiptionService: - data_dir = "data" - input_file_name = f"{data_dir}/inputs/5846093734223028963.ogg" - output_file_name = f"{data_dir}/outputs/5846093734223028963" - model_id = "tiny" - show_text = True - text_preview_size = 10 - - transctiption = Transctiption(model_id=model_id, input_file_name=input_file_name, - show_text=show_text, output_file_name=output_file_name, - text_preview_size=text_preview_size) - - #transctiption.get_transcription() - - - diff --git a/src/app/main.py b/src/app/main.py index 25e15f0..45ae946 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -4,6 +4,5 @@ app = Flask(__name__) @app.route("/") def hello_world(): - name = request.args.get('name') - text = ConvertorService.create_text(name) + text = ConvertorService.create_text() return f"

{text}

" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_core.py b/tests/test_core.py index e69de29..7e77116 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -0,0 +1,45 @@ +def test_imports(): + import whisper + assert True + +def test_imports_modules(): + from src.app.convertor.service.transcription import Transctiption + assert True + +def test_transcription_text(): + from src.app.convertor.service.transcription import Transctiption + data_dir = "data" + input_file_name = f"{data_dir}/inputs/5846093734223028963.ogg" + output_file_name = f"{data_dir}/outputs/5846093734223028963" + model_id = "tiny" + show_text = True + text_preview_size = 10 + + transcription_service = Transctiption(model_id=model_id, input_file_name=input_file_name, + show_text=show_text, output_file_name=output_file_name, + text_preview_size=text_preview_size) + + assert transcription_service._get_model() is not None + assert transcription_service._check_file_extension() is True + assert transcription_service._check_whisper_model_id() is True + + +def test_load_file_success(): + import tempfile + from src.app.convertor.service.transcription import Transctiption + + with tempfile.NamedTemporaryFile(suffix=".ogg") as tmp: + transcription_service = Transctiption(input_file_name=tmp.name) + assert transcription_service.load_file(tmp.name) == tmp.name + +# def test_validate_extension_allowed(): +# from src.app.convertor.service.transcription import Transctiption + +# data_dir = "data" +# model_id = "tiny" +# input_file_name = f"{data_dir}/inputs/5846093734223028963.ogg" +# transcription_service = Transctiption(model_id=model_id, input_file_name=input_file_name, +# show_text="", output_file_name="", +# text_preview_size="") + +# assert transcription_service._check_file_extension() is True diff --git a/uv.lock b/uv.lock index 429fb70..cbfc4c0 100644 --- a/uv.lock +++ b/uv.lock @@ -148,6 +148,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -511,6 +520,15 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/35/8e/d36f8880bcf18ec026a55807d02fe4c7357da9f25aebd92f85178000c0dc/openai_whisper-20250625.tar.gz", hash = "sha256:37a91a3921809d9f44748ffc73c0a55c9f366c85a3ef5c2ae0cc09540432eb96", size = 803191, upload-time = "2025-06-26T01:06:13.34Z" } +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + [[package]] name = "pandas" version = "2.3.3" @@ -558,6 +576,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -699,6 +751,7 @@ dependencies = [ { name = "flask" }, { name = "openai-whisper" }, { name = "pandas" }, + { name = "pytest" }, { name = "torch" }, ] @@ -708,6 +761,7 @@ requires-dist = [ { name = "flask", specifier = ">=3.1.2" }, { name = "openai-whisper", specifier = ">=20250625" }, { name = "pandas", specifier = ">=2.3.3" }, + { name = "pytest", specifier = "==8.4.2" }, { name = "torch", specifier = ">=2.9.1" }, ] From 1af654b6bfc51f0c93d91e424060be694381bef2 Mon Sep 17 00:00:00 2001 From: MRD2F Date: Wed, 26 Nov 2025 18:55:00 +0100 Subject: [PATCH 5/8] Customized ci.yml workflow file, and minor bug fix in test_core.py pytest file --- .github/ci.yml | 39 ---------------------------- .github/workflows/ci.yml | 55 ++++++++++++++++++++++++++++++++++++++++ tests/test_core.py | 22 ++++++++-------- 3 files changed, 66 insertions(+), 50 deletions(-) delete mode 100644 .github/ci.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/ci.yml b/.github/ci.yml deleted file mode 100644 index 5216a63..0000000 --- a/.github/ci.yml +++ /dev/null @@ -1,39 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: Python application - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -permissions: - contents: read - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - pytest \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b2ea4f6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: CI Pipeline + +on: + push: + branches: [ "main", "dev", "test"] + pull_request: + branches: [ "main", "dev" ] + +permissions: + contents: read + +jobs: + build: + name: Run Pytest and formating + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Run a one-line script + run: echo Hello, world! + + - name: Install uv + run: pip install uv + + - name: Install project dependencies + run: uv sync + + - name: Install additional packages + run: uv add flake8 + #if not [ -f pyproject.toml ] or [ -f uv.lock ] ; then pip install -r requirements.txt; fi + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Run pytest + run: | + PYTHONPATH=. pytest --maxfail=1 --disable-warnings -q + - name: Lint with ruff + run: ruff check --output-format=github . --exclude tests + - name: Check formatting with black + run: black --check . \ No newline at end of file diff --git a/tests/test_core.py b/tests/test_core.py index 7e77116..ed4d388 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,11 +3,11 @@ def test_imports(): assert True def test_imports_modules(): - from src.app.convertor.service.transcription import Transctiption + from src.app.convertor.service.transcription import Transcription assert True def test_transcription_text(): - from src.app.convertor.service.transcription import Transctiption + from src.app.convertor.service.transcription import Transcription data_dir = "data" input_file_name = f"{data_dir}/inputs/5846093734223028963.ogg" output_file_name = f"{data_dir}/outputs/5846093734223028963" @@ -15,7 +15,7 @@ def test_transcription_text(): show_text = True text_preview_size = 10 - transcription_service = Transctiption(model_id=model_id, input_file_name=input_file_name, + transcription_service = Transcription(model_id=model_id, input_file_name=input_file_name, show_text=show_text, output_file_name=output_file_name, text_preview_size=text_preview_size) @@ -24,21 +24,21 @@ def test_transcription_text(): assert transcription_service._check_whisper_model_id() is True -def test_load_file_success(): - import tempfile - from src.app.convertor.service.transcription import Transctiption +# def test_load_file_success(): +# import tempfile +# from src.app.convertor.service.transcription import Transcription - with tempfile.NamedTemporaryFile(suffix=".ogg") as tmp: - transcription_service = Transctiption(input_file_name=tmp.name) - assert transcription_service.load_file(tmp.name) == tmp.name +# with tempfile.NamedTemporaryFile(suffix=".ogg") as tmp: +# transcription_service = Transcription(input_file_name=tmp.name) +# assert transcription_service.load_file(tmp.name) == tmp.name # def test_validate_extension_allowed(): -# from src.app.convertor.service.transcription import Transctiption +# from src.app.convertor.service.transcription import Transcription # data_dir = "data" # model_id = "tiny" # input_file_name = f"{data_dir}/inputs/5846093734223028963.ogg" -# transcription_service = Transctiption(model_id=model_id, input_file_name=input_file_name, +# transcription_service = Transcription(model_id=model_id, input_file_name=input_file_name, # show_text="", output_file_name="", # text_preview_size="") From c3af2ed351aabd581854375256425a355e538727 Mon Sep 17 00:00:00 2001 From: MRD2F Date: Wed, 26 Nov 2025 19:29:27 +0100 Subject: [PATCH 6/8] Updates of CI.yml and formating changes made to pass ci requirements --- .github/workflows/ci.yml | 17 +-- pyproject.toml | 3 + .../convertor/service/convertor_service.py | 14 +- src/app/convertor/service/transcription.py | 89 +++++++---- src/app/main.py | 6 +- tests/test_core.py | 19 ++- uv.lock | 138 ++++++++++++++++++ 7 files changed, 231 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2ea4f6..afbbee1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Python 3.12 uses: actions/setup-python@v5 @@ -36,20 +36,13 @@ jobs: run: uv sync - name: Install additional packages - run: uv add flake8 + run: uv add --dev ruff #if not [ -f pyproject.toml ] or [ -f uv.lock ] ; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Run pytest run: | - PYTHONPATH=. pytest --maxfail=1 --disable-warnings -q + PYTHONPATH=. uv run pytest --maxfail=1 --disable-warnings -q - name: Lint with ruff - run: ruff check --output-format=github . --exclude tests + run: uv run ruff check --output-format=github . --exclude tests - name: Check formatting with black - run: black --check . \ No newline at end of file + run: black --check . --diff \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7b18714..452c717 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,10 +5,13 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [ + "black>=25.11.0", "ffmpeg>=1.4", + "flake8>=7.3.0", "flask>=3.1.2", "openai-whisper>=20250625", "pandas>=2.3.3", "pytest==8.4.2", + "ruff>=0.14.6", "torch>=2.9.1", ] diff --git a/src/app/convertor/service/convertor_service.py b/src/app/convertor/service/convertor_service.py index 09a8256..2f85e53 100644 --- a/src/app/convertor/service/convertor_service.py +++ b/src/app/convertor/service/convertor_service.py @@ -2,17 +2,21 @@ class ConvertorService: - + @classmethod def create_text(cls): - #data_dir = "data" + # data_dir = "data" input_file_name = "./convertor/service/data/inputs/5846093734223028963.ogg" - #output_file_name = "./data/outputs/5846093734223028963" + # output_file_name = "./data/outputs/5846093734223028963" model_id = "tiny" show_text = True text_preview_size = 10 - transcription = Transcription(model_id=model_id, input_file_name=input_file_name, - show_text=show_text,text_preview_size=text_preview_size) + transcription = Transcription( + model_id=model_id, + input_file_name=input_file_name, + show_text=show_text, + text_preview_size=text_preview_size, + ) return transcription.get_transcription() diff --git a/src/app/convertor/service/transcription.py b/src/app/convertor/service/transcription.py index df5cc79..4b05901 100644 --- a/src/app/convertor/service/transcription.py +++ b/src/app/convertor/service/transcription.py @@ -1,31 +1,53 @@ import whisper import os + class Transcription: - def __init__(self, model_id="tiny", input_file_name=".ogg", show_text=False, - output_file_name="", text_preview_size=None, - language="english"): + def __init__( + self, + model_id="tiny", + input_file_name=".ogg", + show_text=False, + output_file_name="", + text_preview_size=None, + language="english", + ): self.model_id = model_id - self.input_file_name = input_file_name #self.load_file(input_file_name) + self.input_file_name = input_file_name # self.load_file(input_file_name) self.show_text = show_text self.output_file_name = output_file_name self.text_preview_size = text_preview_size self.language = language ########## Sanity checks for whisper model use ######### - self.whisper_allowed_extensions = ['flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg', 'wav', 'webm'] + self.whisper_allowed_extensions = [ + "flac", + "m4a", + "mp3", + "mp4", + "mpeg", + "mpga", + "oga", + "ogg", + "wav", + "webm", + ] self.whisper_model_ids = ["tiny", "base", "small", "medium", "large", "turbo"] - - # The default setting (which selects the turbo model) works well for transcribing English. - # However, the turbo model is not trained for translation tasks. - # If you need to translate non-English speech into English, use one of the + + # The default setting (which selects the turbo model) works well for transcribing English. + # However, the turbo model is not trained for translation tasks. + # If you need to translate non-English speech into English, use one of the # multilingual models (tiny, base, small, medium, large) instead of turbo. - self.whisper_model_ids_english_only = ["tiny.en", "base.en", "small.en", "medium.en"] + self.whisper_model_ids_english_only = [ + "tiny.en", + "base.en", + "small.en", + "medium.en", + ] self._check_file_extension() self._check_whisper_model_id() - @staticmethod def load_file(input_file_name): if not os.path.exists(input_file_name): @@ -38,11 +60,10 @@ def _check_file_extension(self): if ext not in self.whisper_allowed_extensions: allowed = ", ".join(self.whisper_allowed_extensions) raise ValueError( - f"Invalid file format: .{ext}\n" - f"Allowed formats are: {allowed}" + f"Invalid file format: .{ext}\n" f"Allowed formats are: {allowed}" ) return True - + def _check_whisper_model_id(self): if self.model_id not in self.whisper_model_ids: allowed = ", ".join(self.whisper_model_ids) @@ -51,21 +72,22 @@ def _check_whisper_model_id(self): f"Allowed formats are: {allowed}" ) return True - + def _get_model(self): - model = whisper.load_model(self.model_id) - return model - + model = whisper.load_model(self.model_id) + return model + def save_transcription(self, text, output_file_name=""): file_name = self.output_file_name if not output_file_name else output_file_name with open(f"{file_name}.txt", "w", encoding="utf-8") as f: f.write(text) - def get_transcription(self): model = self._get_model() print(f"Using as requested model {self.model_id}.") - print(f"Transcribing file {self.input_file_name}... this may take a few minutes depening of the file size.") + print( + f"Transcribing file {self.input_file_name}... this may take a few minutes depening of the file size." + ) # load audio and pad/trim it to fit 30 seconds # audio = whisper.load_audio("audio.mp3") # audio = whisper.pad_or_trim(audio) @@ -74,24 +96,28 @@ def get_transcription(self): # _, probs = model.detect_language(mel) # print(f"Detected language: {max(probs, key=probs.get)}") - result = model.transcribe(self.input_file_name, ) + result = model.transcribe( + self.input_file_name, + ) if self.show_text: if self.text_preview_size: - print(result['text'][:self.text_preview_size]) + print(result["text"][: self.text_preview_size]) else: print(result["text"]) if (len(self.output_file_name) > 0) or (len(self.output_file_name) > 0): - file_name = self.output_file_name if not output_file_name else output_file_name + file_name = ( + self.output_file_name if not output_file_name else output_file_name + ) self.save_transcription(result["text"], file_name) print(f"Saved transcription as: {file_name}.") - + return result["text"] if __name__ == "__main__": - + data_dir = "data" input_file_name = "./src/app/convertor/service/data/inputs/5846093734223028963.ogg" output_file_name = "./src/app/convertor/service/data/inputs/5846093734223028963.ogx" @@ -100,11 +126,12 @@ def get_transcription(self): show_text = True text_preview_size = 10 - transcription_service = Transcription(model_id=model_id, input_file_name=input_file_name, - show_text=show_text, output_file_name=output_file_name, - text_preview_size=text_preview_size) + transcription_service = Transcription( + model_id=model_id, + input_file_name=input_file_name, + show_text=show_text, + output_file_name=output_file_name, + text_preview_size=text_preview_size, + ) transcription_service.get_transcription() - - - diff --git a/src/app/main.py b/src/app/main.py index 45ae946..bf4667a 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -1,8 +1,10 @@ -from flask import Flask, request +from flask import Flask from convertor.service.convertor_service import ConvertorService app = Flask(__name__) + + @app.route("/") def hello_world(): text = ConvertorService.create_text() - return f"

{text}

" \ No newline at end of file + return f"

{text}

" diff --git a/tests/test_core.py b/tests/test_core.py index ed4d388..d9c5e13 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,13 +1,18 @@ def test_imports(): import whisper + assert True + def test_imports_modules(): from src.app.convertor.service.transcription import Transcription + assert True + def test_transcription_text(): from src.app.convertor.service.transcription import Transcription + data_dir = "data" input_file_name = f"{data_dir}/inputs/5846093734223028963.ogg" output_file_name = f"{data_dir}/outputs/5846093734223028963" @@ -15,9 +20,13 @@ def test_transcription_text(): show_text = True text_preview_size = 10 - transcription_service = Transcription(model_id=model_id, input_file_name=input_file_name, - show_text=show_text, output_file_name=output_file_name, - text_preview_size=text_preview_size) + transcription_service = Transcription( + model_id=model_id, + input_file_name=input_file_name, + show_text=show_text, + output_file_name=output_file_name, + text_preview_size=text_preview_size, + ) assert transcription_service._get_model() is not None assert transcription_service._check_file_extension() is True @@ -31,14 +40,14 @@ def test_transcription_text(): # with tempfile.NamedTemporaryFile(suffix=".ogg") as tmp: # transcription_service = Transcription(input_file_name=tmp.name) # assert transcription_service.load_file(tmp.name) == tmp.name - + # def test_validate_extension_allowed(): # from src.app.convertor.service.transcription import Transcription # data_dir = "data" # model_id = "tiny" # input_file_name = f"{data_dir}/inputs/5846093734223028963.ogg" -# transcription_service = Transcription(model_id=model_id, input_file_name=input_file_name, +# transcription_service = Transcription(model_id=model_id, input_file_name=input_file_name, # show_text="", output_file_name="", # text_preview_size="") diff --git a/uv.lock b/uv.lock index cbfc4c0..3866635 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,35 @@ version = 1 revision = 3 requires-python = ">=3.12" +[[package]] +name = "black" +version = "25.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, + { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, + { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" }, + { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, +] + [[package]] name = "blinker" version = "1.9.0" @@ -113,6 +142,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, ] +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + [[package]] name = "flask" version = "3.1.2" @@ -259,6 +302,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + [[package]] name = "more-itertools" version = "10.8.0" @@ -277,6 +329,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "networkx" version = "3.5" @@ -576,6 +637,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -585,6 +664,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -622,6 +719,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "pytokens" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, +] + [[package]] name = "pytz" version = "2025.2" @@ -724,6 +830,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "ruff" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f0/62b5a1a723fe183650109407fa56abb433b00aa1c0b9ba555f9c4efec2c6/ruff-0.14.6.tar.gz", hash = "sha256:6f0c742ca6a7783a736b867a263b9a7a80a45ce9bee391eeda296895f1b4e1cc", size = 5669501, upload-time = "2025-11-21T14:26:17.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/d2/7dd544116d107fffb24a0064d41a5d2ed1c9d6372d142f9ba108c8e39207/ruff-0.14.6-py3-none-linux_armv6l.whl", hash = "sha256:d724ac2f1c240dbd01a2ae98db5d1d9a5e1d9e96eba999d1c48e30062df578a3", size = 13326119, upload-time = "2025-11-21T14:25:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/36/6a/ad66d0a3315d6327ed6b01f759d83df3c4d5f86c30462121024361137b6a/ruff-0.14.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9f7539ea257aa4d07b7ce87aed580e485c40143f2473ff2f2b75aee003186004", size = 13526007, upload-time = "2025-11-21T14:25:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9d/dae6db96df28e0a15dea8e986ee393af70fc97fd57669808728080529c37/ruff-0.14.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f6007e55b90a2a7e93083ba48a9f23c3158c433591c33ee2e99a49b889c6332", size = 12676572, upload-time = "2025-11-21T14:25:29.826Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/f319e87759949062cfee1b26245048e92e2acce900ad3a909285f9db1859/ruff-0.14.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8e7b9d73d8728b68f632aa8e824ef041d068d231d8dbc7808532d3629a6bef", size = 13140745, upload-time = "2025-11-21T14:25:32.788Z" }, + { url = "https://files.pythonhosted.org/packages/95/d3/248c1efc71a0a8ed4e8e10b4b2266845d7dfc7a0ab64354afe049eaa1310/ruff-0.14.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d50d45d4553a3ebcbd33e7c5e0fe6ca4aafd9a9122492de357205c2c48f00775", size = 13076486, upload-time = "2025-11-21T14:25:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/a5/19/b68d4563fe50eba4b8c92aa842149bb56dd24d198389c0ed12e7faff4f7d/ruff-0.14.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:118548dd121f8a21bfa8ab2c5b80e5b4aed67ead4b7567790962554f38e598ce", size = 13727563, upload-time = "2025-11-21T14:25:38.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/943169436832d4b0e867235abbdb57ce3a82367b47e0280fa7b4eabb7593/ruff-0.14.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:57256efafbfefcb8748df9d1d766062f62b20150691021f8ab79e2d919f7c11f", size = 15199755, upload-time = "2025-11-21T14:25:41.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b9/288bb2399860a36d4bb0541cb66cce3c0f4156aaff009dc8499be0c24bf2/ruff-0.14.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff18134841e5c68f8e5df1999a64429a02d5549036b394fafbe410f886e1989d", size = 14850608, upload-time = "2025-11-21T14:25:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b1/a0d549dd4364e240f37e7d2907e97ee80587480d98c7799d2d8dc7a2f605/ruff-0.14.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c4b7ec1e66a105d5c27bd57fa93203637d66a26d10ca9809dc7fc18ec58440", size = 14118754, upload-time = "2025-11-21T14:25:47.214Z" }, + { url = "https://files.pythonhosted.org/packages/13/ac/9b9fe63716af8bdfddfacd0882bc1586f29985d3b988b3c62ddce2e202c3/ruff-0.14.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167843a6f78680746d7e226f255d920aeed5e4ad9c03258094a2d49d3028b105", size = 13949214, upload-time = "2025-11-21T14:25:50.002Z" }, + { url = "https://files.pythonhosted.org/packages/12/27/4dad6c6a77fede9560b7df6802b1b697e97e49ceabe1f12baf3ea20862e9/ruff-0.14.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:16a33af621c9c523b1ae006b1b99b159bf5ac7e4b1f20b85b2572455018e0821", size = 14106112, upload-time = "2025-11-21T14:25:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/6a/db/23e322d7177873eaedea59a7932ca5084ec5b7e20cb30f341ab594130a71/ruff-0.14.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1432ab6e1ae2dc565a7eea707d3b03a0c234ef401482a6f1621bc1f427c2ff55", size = 13035010, upload-time = "2025-11-21T14:25:55.536Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9c/20e21d4d69dbb35e6a1df7691e02f363423658a20a2afacf2a2c011800dc/ruff-0.14.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c55cfbbe7abb61eb914bfd20683d14cdfb38a6d56c6c66efa55ec6570ee4e71", size = 13054082, upload-time = "2025-11-21T14:25:58.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/906ee6a0464c3125c8d673c589771a974965c2be1a1e28b5c3b96cb6ef88/ruff-0.14.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:efea3c0f21901a685fff4befda6d61a1bf4cb43de16da87e8226a281d614350b", size = 13303354, upload-time = "2025-11-21T14:26:01.816Z" }, + { url = "https://files.pythonhosted.org/packages/4c/58/60577569e198d56922b7ead07b465f559002b7b11d53f40937e95067ca1c/ruff-0.14.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:344d97172576d75dc6afc0e9243376dbe1668559c72de1864439c4fc95f78185", size = 14054487, upload-time = "2025-11-21T14:26:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/8e4e0639e4cc12547f41cb771b0b44ec8225b6b6a93393176d75fe6f7d40/ruff-0.14.6-py3-none-win32.whl", hash = "sha256:00169c0c8b85396516fdd9ce3446c7ca20c2a8f90a77aa945ba6b8f2bfe99e85", size = 13013361, upload-time = "2025-11-21T14:26:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/82240553b77fd1341f80ebb3eaae43ba011c7a91b4224a9f317d8e6591af/ruff-0.14.6-py3-none-win_amd64.whl", hash = "sha256:390e6480c5e3659f8a4c8d6a0373027820419ac14fa0d2713bd8e6c3e125b8b9", size = 14432087, upload-time = "2025-11-21T14:26:10.891Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1f/93f9b0fad9470e4c829a5bb678da4012f0c710d09331b860ee555216f4ea/ruff-0.14.6-py3-none-win_arm64.whl", hash = "sha256:d43c81fbeae52cfa8728d8766bbf46ee4298c888072105815b392da70ca836b2", size = 13520930, upload-time = "2025-11-21T14:26:13.951Z" }, +] + [[package]] name = "setuptools" version = "80.9.0" @@ -747,21 +879,27 @@ name = "speach-to-text" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "black" }, { name = "ffmpeg" }, + { name = "flake8" }, { name = "flask" }, { name = "openai-whisper" }, { name = "pandas" }, { name = "pytest" }, + { name = "ruff" }, { name = "torch" }, ] [package.metadata] requires-dist = [ + { name = "black", specifier = ">=25.11.0" }, { name = "ffmpeg", specifier = ">=1.4" }, + { name = "flake8", specifier = ">=7.3.0" }, { name = "flask", specifier = ">=3.1.2" }, { name = "openai-whisper", specifier = ">=20250625" }, { name = "pandas", specifier = ">=2.3.3" }, { name = "pytest", specifier = "==8.4.2" }, + { name = "ruff", specifier = ">=0.14.6" }, { name = "torch", specifier = ">=2.9.1" }, ] From 4dbe8daa015180bad586b2b0111b07f85e1bee64 Mon Sep 17 00:00:00 2001 From: MRD2F Date: Wed, 26 Nov 2025 19:31:35 +0100 Subject: [PATCH 7/8] fixed bug on ci.yml in step Check formatting with black --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afbbee1..048866e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,4 +45,4 @@ jobs: - name: Lint with ruff run: uv run ruff check --output-format=github . --exclude tests - name: Check formatting with black - run: black --check . --diff \ No newline at end of file + run: uv run black --check . --diff \ No newline at end of file From 65b56e17671b87d10ffcc038006a1016298f202a Mon Sep 17 00:00:00 2001 From: MRD2F Date: Wed, 26 Nov 2025 19:31:49 +0100 Subject: [PATCH 8/8] fixed bug on ci.yml in step Check formatting with black --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 048866e..b3e8075 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,9 @@ jobs: - name: Run pytest run: | PYTHONPATH=. uv run pytest --maxfail=1 --disable-warnings -q - - name: Lint with ruff + + - name: Lint with ruffW run: uv run ruff check --output-format=github . --exclude tests + - name: Check formatting with black run: uv run black --check . --diff \ No newline at end of file