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