My Voices - Top bar with search, filter toggle (active-count badge), synth-source drawer, group and table buttons; filters/sort in a collapsible labelled panel; one-line sort header with select-all/none checkbox - Groups chip list is a collapsed accordion; bottom bar is Refresh/Sync/Import plus one Actions (N) menu (Edit / Audio / Share / Danger), disabled until voices are checked; touch targets stay 44px - Copy selected works again (copyText was no longer exported), shows "N voice names copied to clipboard" and falls back to a copy dialog when the async clipboard is unavailable TTS / engines - Health probe works for /v1 card URLs and, behind a LiteLLM gateway, checks the selected model's own upstream so stopped containers are not reported as available - Unhiding a voice publishes a missing entry to the engine's voices.json (no more silent default-voice fallback) - Status-bar TTS picker also drives the My Voices and STT-test selectors; Use as TTS refreshes the status bar Includes the engine connect/disconnect, custom TTS, STT footer and magazine auth work that was already in the working tree, with their tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
68 lines
3.2 KiB
Python
68 lines
3.2 KiB
Python
"""Vision credentials stay attached to their configured endpoint."""
|
|
import os
|
|
import pathlib
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
os.environ.setdefault("CONFIG_DIR", tempfile.mkdtemp(prefix="ttvc-magazine-auth-test-"))
|
|
|
|
from core.magazine import BLOCK_FIGURE, describe_visual_region, vision_ocr_page, vision_ocr_page_chain
|
|
|
|
|
|
class VisionAuthTests(unittest.TestCase):
|
|
def test_transcription_and_description_use_only_matching_vision_key(self):
|
|
settings = {
|
|
"vision_ocr_url": "http://primary/",
|
|
"vision_ocr_api_key": "primary-test-key",
|
|
"vision_ocr_fallback_url": "http://fallback/proxy/v1/",
|
|
"vision_ocr_fallback_api_key": "fallback-test-key",
|
|
"llm_api_key": "unrelated-test-key",
|
|
}
|
|
response = mock.Mock()
|
|
response.json.return_value = {"choices": [{"message": {"content":
|
|
"text [10,10,110,60]A real sentence transcribed from this page."}}]}
|
|
for url, expected in (
|
|
("http://primary/v1/", {"Authorization": "Bearer primary-test-key"}),
|
|
("http://fallback/proxy/v1", {"Authorization": "Bearer fallback-test-key"}),
|
|
("http://unconfigured/v1", {}),
|
|
("http://primary/other/v1", {}),
|
|
):
|
|
for describe in (False, True):
|
|
with self.subTest(url=url, describe=describe), \
|
|
mock.patch("core.config._load_settings", return_value=settings), \
|
|
mock.patch("core.magazine.request_with_retries", return_value=response) as request:
|
|
if describe:
|
|
result = describe_visual_region(pathlib.Path(__file__), BLOCK_FIGURE, url, "model")
|
|
else:
|
|
result = vision_ocr_page(pathlib.Path(__file__), url, "model")
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(request.call_args.kwargs["headers"], expected)
|
|
|
|
def test_empty_key_and_fallback_never_reuse_another_engine_key(self):
|
|
settings = {
|
|
"vision_ocr_url": "http://primary/v1",
|
|
"vision_ocr_api_key": "primary-test-key",
|
|
"vision_ocr_fallback_url": "http://fallback/v1",
|
|
"vision_ocr_fallback_api_key": "",
|
|
"llm_api_key": "unrelated-test-key",
|
|
}
|
|
failure = mock.Mock()
|
|
failure.json.return_value = {"choices": [{"message": {"content": ""}}]}
|
|
success = mock.Mock()
|
|
success.json.return_value = {"choices": [{"message": {"content":
|
|
"text [10,10,110,60]A real sentence transcribed from this page."}}]}
|
|
with mock.patch("core.config._load_settings", return_value=settings), \
|
|
mock.patch("core.magazine.request_with_retries", side_effect=[failure, success]) as request:
|
|
result = vision_ocr_page_chain(pathlib.Path(__file__), [
|
|
("http://primary/v1", "model"), ("http://fallback/v1", "model"),
|
|
])
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(request.call_args_list[0].kwargs["headers"],
|
|
{"Authorization": "Bearer primary-test-key"})
|
|
self.assertEqual(request.call_args_list[1].kwargs["headers"], {})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|