From 0f5901b681b9643dd26a81f2f77c59617bda5879 Mon Sep 17 00:00:00 2001 From: Yutsuo Date: Sun, 30 Aug 2026 13:00:43 -0300 Subject: [PATCH] build: scaffold typed application core --- .gitignore | 12 ++++++++++++ pyproject.toml | 27 +++++++++++++++++++++++++++ requirements.txt | 6 ++++++ src/voice_transcriptor/__init__.py | 4 ++++ src/voice_transcriptor/formatting.py | 20 ++++++++++++++++++++ src/voice_transcriptor/models.py | 28 ++++++++++++++++++++++++++++ tests/test_formatting.py | 16 ++++++++++++++++ 7 files changed, 113 insertions(+) create mode 100644 .gitignore create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 src/voice_transcriptor/__init__.py create mode 100644 src/voice_transcriptor/formatting.py create mode 100644 src/voice_transcriptor/models.py create mode 100644 tests/test_formatting.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bea6cec --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +.venv/ +venv/ +dist/ +build/ +*.spec +.coverage +.idea/ +.vscode/ + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c69380e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools>=75"] +build-backend = "setuptools.build_meta" + +[project] +name = "voice-transcriptor" +version = "0.1.0" +description = "Windows desktop media transcription client" +requires-python = ">=3.12" +dependencies = [ + "openai>=1.0", + "PySide6>=6.7", + "keyring>=25.0", +] + +[project.scripts] +voice-transcriptor = "voice_transcriptor.app:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +addopts = "-ra" +testpaths = ["tests"] +pythonpath = ["src"] +qt_api = "pyside6" + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7bbc46a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +openai>=1.0 +PySide6>=6.7 +keyring>=25.0 +pytest>=8.0 +pytest-qt>=4.4 + diff --git a/src/voice_transcriptor/__init__.py b/src/voice_transcriptor/__init__.py new file mode 100644 index 0000000..3a67553 --- /dev/null +++ b/src/voice_transcriptor/__init__.py @@ -0,0 +1,4 @@ +"""Voice Transcriptor desktop application.""" + +__version__ = "0.1.0" + diff --git a/src/voice_transcriptor/formatting.py b/src/voice_transcriptor/formatting.py new file mode 100644 index 0000000..7db4906 --- /dev/null +++ b/src/voice_transcriptor/formatting.py @@ -0,0 +1,20 @@ +def format_file_size(size_bytes: int) -> str: + if size_bytes < 0: + raise ValueError("File size cannot be negative.") + if size_bytes < 1024: + return f"{size_bytes} B" + value = float(size_bytes) + for unit in ("KiB", "MiB", "GiB", "TiB"): + value /= 1024 + if value < 1024 or unit == "TiB": + return f"{value:.2f} {unit}" + raise AssertionError("unreachable") + + +def format_duration(duration_seconds: float | None) -> str: + if duration_seconds is None: + return "Unknown" + seconds = max(0, round(duration_seconds)) + hours, remainder = divmod(seconds, 3600) + minutes, seconds = divmod(remainder, 60) + return f"{hours:02d}:{minutes:02d}:{seconds:02d}" diff --git a/src/voice_transcriptor/models.py b/src/voice_transcriptor/models.py new file mode 100644 index 0000000..6669607 --- /dev/null +++ b/src/voice_transcriptor/models.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True, slots=True) +class AppSettings: + model: str + language: str + output_directory: Path + + +@dataclass(frozen=True, slots=True) +class ToolStatus: + ffmpeg_path: Path | None + ffprobe_path: Path | None + + @property + def available(self) -> bool: + return self.ffmpeg_path is not None and self.ffprobe_path is not None + + +@dataclass(frozen=True, slots=True) +class MediaInfo: + path: Path + size_bytes: int + duration_seconds: float | None + audio_codec: str | None + diff --git a/tests/test_formatting.py b/tests/test_formatting.py new file mode 100644 index 0000000..3afd8c0 --- /dev/null +++ b/tests/test_formatting.py @@ -0,0 +1,16 @@ +from unittest import TestCase + +from voice_transcriptor.formatting import format_duration, format_file_size + + +class FormattingTests(TestCase): + def test_formats_binary_file_sizes(self) -> None: + self.assertEqual(format_file_size(0), "0 B") + self.assertEqual(format_file_size(1536), "1.50 KiB") + self.assertEqual(format_file_size(2 * 1024**3), "2.00 GiB") + + def test_formats_multi_hour_duration(self) -> None: + self.assertEqual(format_duration(3 * 3600 + 5 * 60 + 9.8), "03:05:10") + + def test_formats_unavailable_duration(self) -> None: + self.assertEqual(format_duration(None), "Unknown")