Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/publish-windows-alpha.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ jobs:
- The build is unsigned, so Windows SmartScreen may show a warning.
- Faster Whisper safely falls back from unsupported float16 GPUs to CPU.
- Long-stream waveforms are generated with bounded memory through FFmpeg.
- Optional transcription engines that are not bundled are clearly disabled.
- OpenAI and xAI keys/models can be tested without sending transcript text.
- AI Editor shows the active provider and explains authentication failures.

This payload was promoted only after native Windows CI started the
packaged backend, rendered a vertical MP4 with ASS captions and a
Expand Down
7 changes: 7 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
- Bug fixes from early users
- Clearer local setup guidance

## v0.1.2

- Verifiable OpenAI and xAI API keys without sending a transcript
- Explicit active provider and friendly authentication errors
- Honest desktop transcription-engine availability
- Automatic Faster Whisper model guidance

## v0.2.0

- Better AI clipping workflow
Expand Down
6 changes: 3 additions & 3 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ python-multipart>=0.0.12

# Transcription with word-level timestamps
faster-whisper>=1.0.0
# WhisperX already provides the default transcription path. The legacy
# openai-whisper fallback is optional because its source distribution currently
# fails in pip's isolated build environment on otherwise supported systems.
# Faster Whisper is the bundled/default desktop path. WhisperX and the legacy
# openai-whisper fallback are optional because their dependency stacks currently
# fail in pip's isolated build environment on otherwise supported systems.
# Install it manually with:
# pip install "setuptools<81" wheel && pip install --no-build-isolation openai-whisper
# Optional Parakeet TDT v3 engine support uses NVIDIA NeMo:
Expand Down
17 changes: 17 additions & 0 deletions backend/routers/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ class ModelListRequest(BaseModel):
api_key: Optional[str] = None


class ProviderCheckRequest(BaseModel):
provider: str
api_key: Optional[str] = None
model: Optional[str] = None
base_url: Optional[str] = None


@router.post("/ai/filler-removal")
async def filler_removal(req: FillerRequest):
try:
Expand Down Expand Up @@ -217,3 +224,13 @@ async def ollama_status(base_url: str = "http://localhost:11434"):
async def nine_router_models(req: ModelListRequest):
models = AIProvider.list_9router_models(req.base_url or "http://localhost:20128/v1", req.api_key)
return {"models": models}


@router.post("/ai/provider-check")
async def provider_check(req: ProviderCheckRequest):
return AIProvider.check_cloud_provider(
provider=req.provider,
api_key=req.api_key,
model=req.model,
base_url=req.base_url,
)
15 changes: 14 additions & 1 deletion backend/routers/transcribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,20 @@ def progress(percent: int, message: str):
progress_callback(percent, message)

try:
progress(5, "Preparing transcription")
engine_label = {
"auto": "the best available transcription engine",
"faster-whisper": "Faster Whisper",
"whisperx": "WhisperX",
"whisper": "Whisper",
"parakeet": "Parakeet",
}.get(req.engine, req.engine)
progress(
5,
(
f"Loading {engine_label} model '{req.model}'. "
"On first use, an available engine downloads its speech model automatically."
),
)
result = transcribe_audio(
file_path=req.file_path,
model_name=req.model,
Expand Down
77 changes: 77 additions & 0 deletions backend/scripts/smoke_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,9 +473,23 @@ def test_transcription_engine_status_includes_parakeet(self) -> None:
status = transcription.get_transcription_engine_status()
self.assertIn("faster-whisper", status["engines"])
self.assertTrue(status["engines"]["faster-whisper"]["first_class"])
self.assertIn("downloads automatically", status["engines"]["faster-whisper"]["download_behavior"])
self.assertIn("parakeet", status["engines"])
self.assertTrue(status["engines"]["parakeet"]["first_class"])
self.assertEqual(status["engines"]["parakeet"]["default_model"], transcription.PARAKEET_DEFAULT_MODEL)
if not status["engines"]["whisperx"]["available"]:
self.assertFalse(status["engines"]["whisperx"]["selectable"])
self.assertIn("desktop build", status["engines"]["whisperx"]["unavailable_reason"])

def test_unavailable_whisperx_explains_that_manual_model_download_is_not_enough(self) -> None:
transcription = self._load_transcription_service_or_skip()
original_available = transcription.WHISPERX_AVAILABLE
try:
transcription.WHISPERX_AVAILABLE = False
with self.assertRaisesRegex(RuntimeError, "Downloading a Whisper model manually"):
transcription._resolve_engine("whisperx")
finally:
transcription.WHISPERX_AVAILABLE = original_available

def test_faster_whisper_normalizes_word_timestamps(self) -> None:
transcription = self._load_transcription_service_or_skip()
Expand Down Expand Up @@ -801,6 +815,69 @@ def test_xai_provider_uses_official_openai_compatible_endpoint(self) -> None:
self.assertEqual(args[1], "grok-4.5")
self.assertEqual(args[3], "https://api.x.ai/v1")
self.assertEqual(args[6], "xAI")
self.assertEqual(args[7], "xai")

def test_cloud_provider_check_verifies_key_and_selected_model_without_completion(self) -> None:
response = SimpleNamespace(
ok=True,
status_code=200,
text="",
json=lambda: {
"object": "list",
"data": [
{"id": "grok-4.5"},
{"id": "grok-4.3"},
],
},
)
with patch.object(ai_provider.requests, "get", return_value=response) as request:
result = ai_provider.AIProvider.check_cloud_provider(
provider="xai",
api_key="xai-test",
model="grok-4.5",
)

self.assertTrue(result["ok"])
self.assertTrue(result["authenticated"])
self.assertTrue(result["model_available"])
self.assertEqual(result["models"], ["grok-4.3", "grok-4.5"])
self.assertEqual(request.call_args.args[0], "https://api.x.ai/v1/models")
self.assertEqual(request.call_args.kwargs["headers"]["Authorization"], "Bearer xai-test")

def test_cloud_provider_check_explains_rejected_xai_key(self) -> None:
response = SimpleNamespace(
ok=False,
status_code=400,
text="",
json=lambda: {
"code": "invalid-argument",
"error": "Incorrect API key provided.",
},
)
with patch.object(ai_provider.requests, "get", return_value=response):
result = ai_provider.AIProvider.check_cloud_provider(
provider="xai",
api_key="xai-secret",
model="grok-4.5",
)

self.assertFalse(result["ok"])
self.assertFalse(result["authenticated"])
self.assertEqual(result["code"], "invalid_key")
self.assertIn("did reach xAI", result["message"])
self.assertNotIn("xai-secret", str(result))

def test_completion_error_explains_openai_api_is_separate_from_chatgpt(self) -> None:
error = SimpleNamespace(status_code=401)
error.__str__ = lambda self: "Incorrect API key provided"
message = ai_provider._friendly_completion_error(
"openai",
"OpenAI",
RuntimeError("Incorrect API key provided"),
"gpt-4o",
)

self.assertIn("ChatGPT subscription does not include OpenAI API usage", message)

def test_clip_request_includes_shorts_platform_guidance(self) -> None:
captured: dict[str, str] = {}
Expand Down
Loading