Browse Source

docs: wire and validate OpenAI transcription

master
Yutsuo 4 days ago
parent
commit
e7c0ee7740
  1. 16
      README.md
  2. 13
      src/voice_transcriptor/app.py
  3. 20
      tests/ui/test_app.py

16
README.md

@ -1,6 +1,6 @@
# Voice Transcriptor # Voice Transcriptor
Windows desktop preprocessing foundation for long audio and video transcription jobs. Windows desktop application for resumable OpenAI transcription of long audio and video recordings.
## Setup and run ## Setup and run
@ -15,9 +15,17 @@ The app accepts `.m4a`, `.mp3`, `.wav`, `.mp4`, `.mov`, `.webm`, and `.mkv`. It
Chunking is duration-based, not tied to a presumed universal upload-size limit. Defaults are 15-minute chunks with 15 seconds of overlap. Change duration, overlap, and temporary-file retention under **Settings → Advanced**. Chunking is duration-based, not tied to a presumed universal upload-size limit. Defaults are 15-minute chunks with 15 seconds of overlap. Change duration, overlap, and temporary-file retention under **Settings → Advanced**.
Each job writes an exact-timestamp JSON manifest in its job-specific temporary directory. Files are removed after completion, cancellation, or failure unless retention is enabled for debugging. Active preprocessing can be cancelled from the main window. Each transcription job is stored under `<output directory>/voice-transcriptor-jobs/<job-id>/`. Its atomic JSON manifest retains the exact source start/end time of every chunk plus `pending`, `processing`, `completed`, or `failed` status and raw transcript text. Successful chunks are saved immediately. Restarting the same source with the same effective settings resumes an incomplete job and never retranscribes completed chunks.
This milestone prepares media only. It does not call a transcription API or upload chunks. The default model is `gpt-transcribe`. Settings provides an editable selector seeded with `gpt-transcribe`, `gpt-4o-transcribe`, and `gpt-4o-mini-transcribe`, so future compatible transcription model IDs can be entered. The current official API documentation may reject `gpt-transcribe` for accounts where that identifier is unavailable; the application reports that as a permanent API error without silently changing models.
Language defaults to Brazilian Portuguese. The app sends ISO language `pt` and a prompt that preserves the spoken language without translation, Brazilian spelling and punctuation, numbers, names, technical terminology, and acronyms. Add names and specialized vocabulary in **Settings → Context / Vocabulary**.
Every preprocessed chunk is submitted independently to the OpenAI Audio Transcriptions API. Raw chunk transcripts are concatenated in source order; overlap deduplication is intentionally not implemented yet. Rate limits, timeouts, connection errors, and server errors use finite exponential backoff. Permanent API failures are not retried indefinitely. Active preprocessing, retry waits, and transcription can be cancelled without blocking the GUI.
## API key
Store the API key as a Windows Credential Manager generic credential whose target name is exactly `OPENAI_API_KEY`. The application retrieves that existing target through the Windows keyring backend. Saving a replacement key from Settings updates the same target. API keys and authorization headers are never written to settings, job manifests, transcripts, or logs.
## Tests ## Tests
@ -26,4 +34,4 @@ python -m pytest -v
python -m compileall -q src tests python -m compileall -q src tests
``` ```
The OpenAI API key is stored through `keyring` rather than in the JSON settings file. Tests use fake API endpoints and do not make live OpenAI requests.

13
src/voice_transcriptor/app.py

@ -3,19 +3,28 @@ from __future__ import annotations
import sys import sys
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from openai import OpenAI
from voice_transcriptor.services.credentials import CredentialService from voice_transcriptor.services.credentials import CredentialService
from voice_transcriptor.services.media_probe import MediaProbeService, detect_tools from voice_transcriptor.services.media_probe import MediaProbeService, detect_tools
from voice_transcriptor.services.preprocessing import PreprocessingService from voice_transcriptor.services.preprocessing import PreprocessingService
from voice_transcriptor.services.job_manifest import JobManifestRepository
from voice_transcriptor.services.settings import SettingsRepository from voice_transcriptor.services.settings import SettingsRepository
from voice_transcriptor.services.transcription import OpenAITranscriptionClient, TranscriptionService
from voice_transcriptor.ui.main_window import MainWindow from voice_transcriptor.ui.main_window import MainWindow
def create_main_window() -> MainWindow: def create_main_window() -> MainWindow:
tools = detect_tools() tools = detect_tools()
probe = MediaProbeService(tools.ffprobe_path) if tools.ffprobe_path else _UnavailableProbe() probe = MediaProbeService(tools.ffprobe_path) if tools.ffprobe_path else _UnavailableProbe()
preprocessing = PreprocessingService(tools.ffmpeg_path, probe) if tools.ffmpeg_path else _UnavailablePreprocessing() manifests = JobManifestRepository()
return MainWindow(probe, preprocessing, SettingsRepository(), CredentialService()) preprocessing = PreprocessingService(tools.ffmpeg_path, probe, manifest_repository=manifests) if tools.ffmpeg_path else _UnavailablePreprocessing()
def transcription_factory(api_key: str) -> TranscriptionService:
client = OpenAI(api_key=api_key, max_retries=0)
return TranscriptionService(OpenAITranscriptionClient(client), manifests)
return MainWindow(probe, preprocessing, SettingsRepository(), CredentialService(), transcription_factory)
class _UnavailableProbe: class _UnavailableProbe:

20
tests/ui/test_app.py

@ -4,3 +4,23 @@ def test_concrete_window_starts_offscreen(qtbot, monkeypatch) -> None:
qtbot.addWidget(window) qtbot.addWidget(window)
window.show() window.show()
assert window.windowTitle() == "Voice Transcriptor" assert window.windowTitle() == "Voice Transcriptor"
def test_openai_client_is_created_lazily_with_sdk_retries_disabled(qtbot, monkeypatch) -> None:
from voice_transcriptor import app
calls = []
class FakeOpenAI:
def __init__(self, **kwargs):
calls.append(kwargs)
self.audio = type("Audio", (), {"transcriptions": object()})()
monkeypatch.setattr(app, "OpenAI", FakeOpenAI)
window = app.create_main_window()
qtbot.addWidget(window)
assert calls == []
service = window.transcription_factory("secret-value")
assert calls == [{"api_key": "secret-value", "max_retries": 0}]
assert service.client.client.audio.transcriptions is not None

Loading…
Cancel
Save