Ship native iOS app, Wiki/TTS backend, and skeleton loading UX.
Replace the WebView shell with SwiftUI screens, add account-scoped Wiki and TTS APIs with adaptive review and photo scan support, and keep web/iOS pages usable while data loads asynchronously. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from services.tts_service import TtsService, TtsServiceError, tts_service
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, body: bytes, content_type: str = "application/json"):
|
||||
self._body = body
|
||||
self.headers = MagicMock()
|
||||
self.headers.get_content_type.return_value = content_type
|
||||
|
||||
def read(self):
|
||||
return self._body
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
|
||||
class TtsServiceTest(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
get_settings = __import__("config", fromlist=["get_settings"]).get_settings
|
||||
get_settings.cache_clear()
|
||||
|
||||
def test_not_configured_when_disabled(self):
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"TKMIND_TTS_ENABLED": "false", "TKMIND_TTS_BASE_URL": "http://127.0.0.1:19200"},
|
||||
clear=False,
|
||||
):
|
||||
__import__("config", fromlist=["get_settings"]).get_settings.cache_clear()
|
||||
self.assertFalse(tts_service.is_configured())
|
||||
|
||||
@patch("services.tts_service._open_tts_request")
|
||||
def test_request_includes_user_agent(self, urlopen_mock: MagicMock):
|
||||
wav = b"RIFFxxxx"
|
||||
payload = {"code": 0, "data": base64.b64encode(wav).decode("ascii")}
|
||||
urlopen_mock.return_value = _FakeResponse(json.dumps(payload).encode("utf-8"))
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"TKMIND_TTS_BASE_URL": "https://asr.tkmind.cn", "TKMIND_TTS_ENABLED": "true"},
|
||||
clear=False,
|
||||
):
|
||||
__import__("config", fromlist=["get_settings"]).get_settings.cache_clear()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with patch("services.tts_service._CACHE_DIR", __import__("pathlib").Path(tmp)):
|
||||
tts_service.synthesize("apple")
|
||||
request = urlopen_mock.call_args.args[0]
|
||||
self.assertEqual(request.get_header("User-agent"), "WordLoop-TTS/1.0")
|
||||
|
||||
@patch("services.tts_service._open_tts_request")
|
||||
def test_synthesize_parses_json_base64(self, urlopen_mock: MagicMock):
|
||||
wav = b"RIFFxxxx"
|
||||
payload = {"code": 0, "message": "ok", "data": base64.b64encode(wav).decode("ascii")}
|
||||
urlopen_mock.return_value = _FakeResponse(json.dumps(payload).encode("utf-8"))
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"TKMIND_TTS_BASE_URL": "http://127.0.0.1:19200", "TKMIND_TTS_ENABLED": "true"},
|
||||
clear=False,
|
||||
):
|
||||
__import__("config", fromlist=["get_settings"]).get_settings.cache_clear()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with patch("services.tts_service._CACHE_DIR", __import__("pathlib").Path(tmp)):
|
||||
audio, content_type = tts_service.synthesize("apple")
|
||||
self.assertEqual(audio, wav)
|
||||
self.assertEqual(content_type, "audio/wav")
|
||||
|
||||
@patch("services.tts_service._open_tts_request")
|
||||
def test_synthesize_uses_cache_on_second_call(self, urlopen_mock: MagicMock):
|
||||
wav = b"RIFFcached"
|
||||
payload = {"code": 0, "data": base64.b64encode(wav).decode("ascii")}
|
||||
urlopen_mock.return_value = _FakeResponse(json.dumps(payload).encode("utf-8"))
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"TKMIND_TTS_BASE_URL": "http://127.0.0.1:19200", "TKMIND_TTS_ENABLED": "true"},
|
||||
clear=False,
|
||||
):
|
||||
__import__("config", fromlist=["get_settings"]).get_settings.cache_clear()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with patch("services.tts_service._CACHE_DIR", __import__("pathlib").Path(tmp)):
|
||||
first, _ = tts_service.synthesize("apple")
|
||||
second, _ = tts_service.synthesize("apple")
|
||||
self.assertEqual(first, second)
|
||||
self.assertEqual(urlopen_mock.call_count, 1)
|
||||
|
||||
@patch("services.tts_service._open_tts_request")
|
||||
def test_upstream_error_message(self, urlopen_mock: MagicMock):
|
||||
payload = {"code": 500, "message": "TTS 未启用(ENABLE_TTS=false)", "data": None}
|
||||
urlopen_mock.return_value = _FakeResponse(json.dumps(payload).encode("utf-8"))
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"TKMIND_TTS_BASE_URL": "http://127.0.0.1:19200", "TKMIND_TTS_ENABLED": "true"},
|
||||
clear=False,
|
||||
):
|
||||
__import__("config", fromlist=["get_settings"]).get_settings.cache_clear()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with patch("services.tts_service._CACHE_DIR", __import__("pathlib").Path(tmp)):
|
||||
with self.assertRaises(TtsServiceError) as ctx:
|
||||
tts_service.synthesize("apple")
|
||||
self.assertIn("TTS 未启用", str(ctx.exception))
|
||||
|
||||
def test_extract_audio_base64_from_dict(self):
|
||||
value = TtsService._extract_audio_base64({"audio": "abc"})
|
||||
self.assertEqual(value, "abc")
|
||||
Reference in New Issue
Block a user