Fix memoir image delivery and Android rendering
This commit is contained in:
58
api/tests/test_chapters_router_images.py
Normal file
58
api/tests/test_chapters_router_images.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from api.routers.chapters import _chapter_to_dict
|
||||
|
||||
|
||||
class ChaptersRouterImagesTest(unittest.TestCase):
|
||||
@patch("api.routers.chapters.TencentCosStorageService")
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"TENCENT_COS_BUCKET": "life-echo-dev-1319381411",
|
||||
"TENCENT_COS_REGION": "ap-shanghai",
|
||||
"TENCENT_COS_BASE_URL": "https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com",
|
||||
},
|
||||
clear=False,
|
||||
)
|
||||
def test_chapter_to_dict_returns_signed_image_urls_for_response(self, storage_cls):
|
||||
storage = Mock()
|
||||
storage.get_download_url.return_value = "https://signed.example.com/memoirs/u1/c1/0-demo.png?sig=123"
|
||||
storage_cls.from_env.return_value = storage
|
||||
|
||||
chapter = type(
|
||||
"ChapterStub",
|
||||
(),
|
||||
{
|
||||
"id": "chapter-1",
|
||||
"title": "童年的夏天",
|
||||
"content": "{{IMAGE:南方小镇的青石板路}}",
|
||||
"order_index": 0,
|
||||
"status": "completed",
|
||||
"category": "childhood",
|
||||
"images": [
|
||||
{
|
||||
"index": 0,
|
||||
"placeholder": "{{IMAGE:南方小镇的青石板路}}",
|
||||
"description": "南方小镇的青石板路",
|
||||
"status": "completed",
|
||||
"prompt": "A serene southern China town",
|
||||
"url": "https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com/memoirs/u1/c1/0-demo.png",
|
||||
"storage_key": "memoirs/u1/c1/0-demo.png",
|
||||
}
|
||||
],
|
||||
"updated_at": None,
|
||||
"is_new": False,
|
||||
"source_segments": [],
|
||||
},
|
||||
)()
|
||||
|
||||
payload = _chapter_to_dict(chapter)
|
||||
|
||||
self.assertEqual(
|
||||
payload["images"][0]["url"],
|
||||
"https://signed.example.com/memoirs/u1/c1/0-demo.png?sig=123",
|
||||
)
|
||||
self.assertEqual(payload["images"][0]["prompt"], "A serene southern China town")
|
||||
self.assertNotIn("storage_key", payload["images"][0])
|
||||
@@ -58,6 +58,7 @@ class GenerateChapterImagesTaskTest(unittest.TestCase):
|
||||
generate_chapter_images.run("chapter-1")
|
||||
|
||||
self.assertEqual(chapter.images[0]["status"], "completed")
|
||||
self.assertEqual(chapter.images[0]["storage_key"], "memoirs/user-1/chapter-1/0-7e1f860790.png")
|
||||
self.assertEqual(chapter.images[0]["url"], "https://cos.example.com/memoirs/u1/c1/0.png")
|
||||
self.assertEqual(chapter.images[0]["prompt"], "A serene southern China town")
|
||||
db.commit.assert_called()
|
||||
|
||||
@@ -66,3 +66,23 @@ class MemoirImageBootstrapTest(unittest.TestCase):
|
||||
self.assertEqual(assets[0]["url"], "https://cos.example.com/existing.png")
|
||||
self.assertEqual(assets[1]["status"], "pending")
|
||||
self.assertEqual(assets[1]["description"], "奶奶坐在院子里的藤椅上")
|
||||
|
||||
def test_initialize_chapter_images_accepts_double_brace_placeholders(self):
|
||||
chapter = type(
|
||||
"ChapterStub",
|
||||
(),
|
||||
{
|
||||
"id": "chapter-1",
|
||||
"title": "童年的夏天",
|
||||
"category": "childhood",
|
||||
"content": "开头。\n\n{{IMAGE:1938年初的上海弄堂口,冬日萧瑟}}\n\n结尾。",
|
||||
"images": [],
|
||||
},
|
||||
)()
|
||||
|
||||
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False):
|
||||
assets = initialize_chapter_images(chapter)
|
||||
|
||||
self.assertEqual(len(assets), 1)
|
||||
self.assertEqual(assets[0]["status"], "pending")
|
||||
self.assertEqual(assets[0]["placeholder"], "{{IMAGE:1938年初的上海弄堂口,冬日萧瑟}}")
|
||||
|
||||
@@ -43,3 +43,12 @@ class MemoirImageParserTest(unittest.TestCase):
|
||||
self.assertEqual(assets[0]["status"], "pending")
|
||||
self.assertEqual(assets[0]["provider"], "liblib")
|
||||
self.assertEqual(assets[0]["url"], None)
|
||||
|
||||
def test_parse_image_placeholders_accepts_double_brace_variant(self):
|
||||
content = "开头。\n\n{{IMAGE:1938年初的上海弄堂口,冬日萧瑟}}\n\n结尾。"
|
||||
|
||||
items = parse_image_placeholders(content, max_images=2)
|
||||
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertEqual(items[0]["placeholder"], "{{IMAGE:1938年初的上海弄堂口,冬日萧瑟}}")
|
||||
self.assertEqual(items[0]["description"], "1938年初的上海弄堂口,冬日萧瑟")
|
||||
|
||||
@@ -15,6 +15,7 @@ class MemoirImagePromptingTest(unittest.TestCase):
|
||||
default_size="1024x1024",
|
||||
poll_interval_seconds=3,
|
||||
max_attempts=20,
|
||||
liblib_template_uuid="tpl-uuid",
|
||||
)
|
||||
service = MemoirImagePromptService(llm=None, settings=settings)
|
||||
|
||||
@@ -39,6 +40,7 @@ class MemoirImagePromptingTest(unittest.TestCase):
|
||||
default_size="1024x1024",
|
||||
poll_interval_seconds=3,
|
||||
max_attempts=20,
|
||||
liblib_template_uuid="tpl-uuid",
|
||||
)
|
||||
llm = Mock()
|
||||
llm.invoke.return_value.content = (
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from api.services.memoir_images.storage import TencentCosStorageService
|
||||
from api.services.memoir_images.storage import (
|
||||
TencentCosStorageService,
|
||||
normalize_cos_url,
|
||||
resolve_image_storage_key,
|
||||
)
|
||||
|
||||
|
||||
class MemoirImageStorageTest(unittest.TestCase):
|
||||
@@ -28,3 +32,80 @@ class MemoirImageStorageTest(unittest.TestCase):
|
||||
"https://memoir-1250000000.cos.ap-shanghai.myqcloud.com/memoirs/u1/c1/0-demo.png",
|
||||
)
|
||||
client.put_object.assert_called_once()
|
||||
|
||||
@patch("api.services.memoir_images.storage.CosS3Client")
|
||||
def test_upload_bytes_normalizes_duplicate_appid_suffix_in_base_url(self, client_cls):
|
||||
client = Mock()
|
||||
client_cls.return_value = client
|
||||
storage = TencentCosStorageService(
|
||||
secret_id="id",
|
||||
secret_key="key",
|
||||
region="ap-shanghai",
|
||||
bucket="life-echo-dev-1319381411",
|
||||
base_url="https://life-echo-dev-1319381411-appid.cos.ap-shanghai.myqcloud.com",
|
||||
)
|
||||
|
||||
url = storage.upload_bytes(
|
||||
image_bytes=b"png-bytes",
|
||||
key="memoirs/u1/c1/0-demo.png",
|
||||
content_type="image/png",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
url,
|
||||
"https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com/memoirs/u1/c1/0-demo.png",
|
||||
)
|
||||
client.put_object.assert_called_once()
|
||||
|
||||
def test_normalize_cos_url_repairs_existing_duplicate_appid_host(self):
|
||||
normalized = normalize_cos_url(
|
||||
"https://life-echo-dev-1319381411-appid.cos.ap-shanghai.myqcloud.com/memoirs/u1/c1/0-demo.png",
|
||||
bucket="life-echo-dev-1319381411",
|
||||
region="ap-shanghai",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
normalized,
|
||||
"https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com/memoirs/u1/c1/0-demo.png",
|
||||
)
|
||||
|
||||
@patch("api.services.memoir_images.storage.CosS3Client")
|
||||
def test_get_download_url_returns_presigned_download_url(self, client_cls):
|
||||
client = Mock()
|
||||
client.get_presigned_download_url.return_value = "https://cos.example.com/0.png?q-sign-algorithm=sha1"
|
||||
client_cls.return_value = client
|
||||
storage = TencentCosStorageService(
|
||||
secret_id="id",
|
||||
secret_key="key",
|
||||
region="ap-shanghai",
|
||||
bucket="life-echo-dev-1319381411",
|
||||
base_url="https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com",
|
||||
)
|
||||
|
||||
url = storage.get_download_url("memoirs/u1/c1/0-demo.png", expires=1800)
|
||||
|
||||
self.assertEqual(url, "https://cos.example.com/0.png?q-sign-algorithm=sha1")
|
||||
client.get_presigned_download_url.assert_called_once_with(
|
||||
Bucket="life-echo-dev-1319381411",
|
||||
Key="memoirs/u1/c1/0-demo.png",
|
||||
Expired=1800,
|
||||
)
|
||||
|
||||
def test_resolve_image_storage_key_prefers_explicit_storage_key(self):
|
||||
key = resolve_image_storage_key(
|
||||
{
|
||||
"storage_key": "memoirs/u1/c1/0-demo.png",
|
||||
"url": "https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com/other.png",
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(key, "memoirs/u1/c1/0-demo.png")
|
||||
|
||||
def test_resolve_image_storage_key_derives_key_from_existing_url(self):
|
||||
key = resolve_image_storage_key(
|
||||
{
|
||||
"url": "https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com/memoirs/u1/c1/0-demo.png?q-sign-algorithm=sha1"
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(key, "memoirs/u1/c1/0-demo.png")
|
||||
|
||||
@@ -6,7 +6,12 @@ from api.services.pdf_service import PDFService
|
||||
|
||||
class PDFServiceImagesTest(unittest.IsolatedAsyncioTestCase):
|
||||
@patch("api.services.pdf_service.httpx.AsyncClient")
|
||||
async def test_generate_pdf_embeds_completed_images_and_removes_placeholders(self, async_client_cls):
|
||||
@patch("api.services.pdf_service.TencentCosStorageService")
|
||||
async def test_generate_pdf_embeds_completed_images_and_removes_placeholders(
|
||||
self,
|
||||
storage_cls,
|
||||
async_client_cls,
|
||||
):
|
||||
png_bytes = (
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
b"\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc```\x00\x00"
|
||||
@@ -20,6 +25,9 @@ class PDFServiceImagesTest(unittest.IsolatedAsyncioTestCase):
|
||||
mock_client.get.return_value = mock_response
|
||||
async_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
async_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)
|
||||
storage = MagicMock()
|
||||
storage.get_download_url.return_value = "https://signed.example.com/0.png?sig=123"
|
||||
storage_cls.from_env.return_value = storage
|
||||
|
||||
service = PDFService()
|
||||
book = type("BookStub", (), {"title": "我的回忆录"})()
|
||||
@@ -33,7 +41,8 @@ class PDFServiceImagesTest(unittest.IsolatedAsyncioTestCase):
|
||||
{
|
||||
"index": 0,
|
||||
"placeholder": "{{{{IMAGE:南方小镇的青石板路}}}}",
|
||||
"url": "https://cos.example.com/0.png",
|
||||
"url": "https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com/memoirs/u1/c1/0.png",
|
||||
"storage_key": "memoirs/u1/c1/0.png",
|
||||
"status": "completed",
|
||||
}
|
||||
],
|
||||
@@ -44,3 +53,4 @@ class PDFServiceImagesTest(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertGreater(len(pdf_bytes), 100)
|
||||
self.assertNotIn(b"IMAGE:", pdf_bytes)
|
||||
mock_client.get.assert_called_once_with("https://signed.example.com/0.png?sig=123")
|
||||
|
||||
@@ -35,6 +35,7 @@ class ProcessMemoirSegmentsImageEnqueueTest(unittest.TestCase):
|
||||
default_size="1024x1024",
|
||||
poll_interval_seconds=3,
|
||||
max_attempts=20,
|
||||
liblib_template_uuid="tpl-uuid",
|
||||
)
|
||||
get_state_mock.return_value = SimpleNamespace(current_stage="childhood", slots={})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user