7 changed files with 113 additions and 0 deletions
@ -0,0 +1,12 @@ |
|||||||
|
__pycache__/ |
||||||
|
*.py[cod] |
||||||
|
.pytest_cache/ |
||||||
|
.venv/ |
||||||
|
venv/ |
||||||
|
dist/ |
||||||
|
build/ |
||||||
|
*.spec |
||||||
|
.coverage |
||||||
|
.idea/ |
||||||
|
.vscode/ |
||||||
|
|
||||||
@ -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" |
||||||
|
|
||||||
@ -0,0 +1,6 @@ |
|||||||
|
openai>=1.0 |
||||||
|
PySide6>=6.7 |
||||||
|
keyring>=25.0 |
||||||
|
pytest>=8.0 |
||||||
|
pytest-qt>=4.4 |
||||||
|
|
||||||
@ -0,0 +1,4 @@ |
|||||||
|
"""Voice Transcriptor desktop application.""" |
||||||
|
|
||||||
|
__version__ = "0.1.0" |
||||||
|
|
||||||
@ -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}" |
||||||
@ -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 |
||||||
|
|
||||||
@ -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") |
||||||
Loading…
Reference in new issue