From e5b7fc4afa673fef30630d565759bc6b48f138a8 Mon Sep 17 00:00:00 2001 From: chengma Date: Fri, 10 Jul 2026 09:00:06 +0800 Subject: [PATCH] =?UTF-8?q?T-579=20=E5=86=85=E7=BD=AE=E9=BB=98=E8=AE=A4AI?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/default_prompts/__init__.py | 1 + app/default_prompts/cover/__init__.py | 1 + app/default_prompts/cover/papa1.txt | 36 ++++++++ app/default_prompts/title_prompt.txt | 126 ++++++++++++++++++++++++++ app/gui/tabs/generate.py | 1 + app/prompts.py | 61 +++++++++++++ cmshopee.spec | 8 +- docs/tasks/T-579.md | 12 ++- tests/test_gui.py | 25 ++++- tests/test_packaging.py | 5 +- tests/test_prompts.py | 34 +++++++ 11 files changed, 306 insertions(+), 4 deletions(-) create mode 100644 app/default_prompts/__init__.py create mode 100644 app/default_prompts/cover/__init__.py create mode 100644 app/default_prompts/cover/papa1.txt create mode 100644 app/default_prompts/title_prompt.txt diff --git a/app/default_prompts/__init__.py b/app/default_prompts/__init__.py new file mode 100644 index 0000000..acf8720 --- /dev/null +++ b/app/default_prompts/__init__.py @@ -0,0 +1 @@ +"""Bundled default prompt resources.""" diff --git a/app/default_prompts/cover/__init__.py b/app/default_prompts/cover/__init__.py new file mode 100644 index 0000000..1a72eba --- /dev/null +++ b/app/default_prompts/cover/__init__.py @@ -0,0 +1 @@ +"""Bundled default cover prompt templates.""" diff --git a/app/default_prompts/cover/papa1.txt b/app/default_prompts/cover/papa1.txt new file mode 100644 index 0000000..88471ce --- /dev/null +++ b/app/default_prompts/cover/papa1.txt @@ -0,0 +1,36 @@ +商品标题:{新标题} +【核心執行指令:韓系極簡大碼電商主圖】 + +1. 畫面風格與氛圍 (視覺基調) + +場景化生活感: 捨棄死板的工廠掛拍或雜亂背景,全面升級為「韓系生活氛圍感」的實穿場景。採用高級「韓系極簡風」,以奶茶色調、莫蘭迪色系、鼠尾草綠等低飽和色彩為主。搭配明亮柔和的自然光影,營造輕鬆的日常氛圍(如質感街道、室內一隅)。 + +情緒價值: 營造讓買家在 1 秒內產生共鳴的畫面,傳遞「這件衣服適合我、穿起來修身又優雅」的視覺感受。 + +2. 模特兒設定與人體結構防錯 (重點規範) + +精準人體結構 (防錯機制): 【最高權重】確保人體骨骼與四肢結構 100% 符合真實人類解剖學。嚴格禁止生成多餘肢體(如三隻手、三條腿、多根手指)、禁止關節扭曲、肢體融合或殘缺。手部抓握物品(如持咖啡杯)的姿勢必須極度自然逼真。 + +目標受眾樣貌: 模特兒限定為 30 歲以上及樂齡世代、具備台灣女性氣質的微胖/大碼身形(能完美展現大至 80 公斤身形的自信與修身美感)。 + +動態與多樣性: 展現自然微笑與自信優雅姿態,透過不經意的動態(如轉身、微側身)展現衣服垂墜感。臉部樣貌需具備多樣性,絕對避免每次生成同一張臉。 + +3. 產品還原與細節保留 (商品核心) + +100% 實物還原: 完美保留原圖衣服的款式、材質與設計亮點(如紫色螺紋花朵刺繡、新中式盤扣、不規則下擺、藍色暈染字母、V領抽繩褶皺等),禁止擅自改變服裝結構,並承襲原圖的靈魂單品搭配(如小棕包、手鐲、項鍊等配件)。 + +版面佔比: 服裝必須佔據整體版面 60% 以上。 + +4. 排版、文案與浮水印 (後製規範) + +極簡文案: 畫面保持 90% 以上的純淨度。需要添加核心賣點文字,可以根據對應標題獲取核心詞,但絕對不可遮擋服裝細節。 + +隱蔽防盜浮水印: 強制在圖片「正左上角」添加「PAPA」浮水印。字體總寬度嚴格限制在圖片總寬度的 5% 以內,透明度絕對鎖定在 3%。確保其極度低調、無侵入性,絕不破壞 4K 高清細節與畫面留白美感。 + +5. 輸出與交付標準 (系統設定) + +免選方案直接輸出: 未來的對話中,接收商品圖與標題後,請自動篩選並「直接輸出」最具點擊率潛力的唯一優化主圖,無需提供多種方案挑選。 + +規格限制: 輸出為 4K 高清畫質的 JPEG 格式,比例 1:1(建議解析度 1024x1024 或以上),且單張檔案大小不超過 2MB。 + +6.圖片展示【台灣出貨】等時效性詞,请设计一个突出,吸睛的标签。 \ No newline at end of file diff --git a/app/default_prompts/title_prompt.txt b/app/default_prompts/title_prompt.txt new file mode 100644 index 0000000..47f1d16 --- /dev/null +++ b/app/default_prompts/title_prompt.txt @@ -0,0 +1,126 @@ +旧标题:{旧标题} +蝦皮台灣站 商品標題優化標準(最終梳理版) +版本:2026.05 最終優化版 +適用平台:蝦皮台灣站(Shopee TW) + +================================================================================ +一、核心結構(強制執行) +================================================================================ +• 標題開頭固定為:【台灣出貨】 +• 開頭第二個詞必須是該商品「最核心商品賣點的搜索關鍵詞」 +• 關鍵詞需符合台灣用戶搜尋習慣,並自然融入商品賣點與風格,痛點解決,受眾與場景,實時熱銷利益詞 +• 總字數嚴格控制在 54~60 個字符(含中英文、空格、【】、標點),不可超過 60 字符 +•标题中包含有大码。大尺码等大码女装相关的关键词,增加标题中大码女装的相关关键词密度。 +•标题中包含有小個子。矮個子等小個子女装相关的关键词,增加标题中小個子穿搭的相关关键词密度。 + +================================================================================ +二、絕對禁止事項 +================================================================================ +• 禁止使用任何時效承諾詞(現貨、快速出貨、秒發、24H出貨、隔日達等),除非原標題已出現 +• 禁止出現店名或店鋪相關詞,所有的品牌標籤都是全面禁止的,禁止出現任何品牌名稱 +• 原標題未出現材質時,嚴禁加入任何材質名詞 +• 原標題未出現大碼/plus size相關詞時,嚴禁添加大尺碼描述 +• 禁止醫療、治療、療效相關詞(詳見下方特殊處理) +• 請去掉如新港棉,雲朵棉,40支純棉等不會有用戶搜素的材質詞,增加核心指令請確保標題中的所有關鍵詞都是符合用戶搜素習慣,搜素需求的關鍵詞。 +================================================================================ +三、高價值詞保留與加入原則 +================================================================================ +• 必須保留原標題中的風格詞、版型詞(如:重工刺繡、文藝復古、古典復古、A字裙、大擺裙、寬鬆顯瘦、辣妹風、純欲風等) +• 優先加入季節與轉換詞(服飾類):冰絲涼感、抗熱消暑、透氣涼感、親膚舒適、輕薄無負擔 +• 其他品類依商品特性加入對應利益詞與熱銷類目詞 +• 服飾類風格需動態匹配原標題: + - 原標題偏辣妹/純欲 → 強調設計感、性感氣質、顯瘦曲線 + - 原標題偏法式/名媛 → 強調優雅、高質感、知性氣質 + - 原標題偏文藝/復古/媽媽裝 → 強調復古知性、寬鬆顯瘦、實用舒適 + +================================================================================ +四、輸出格式 (不要展示思考過程,直接出結果) +================================================================================ +單一標題: + 直接輸出新標題(只輸出優化後標題) + +================================================================================ +輸出範例 +================================================================================ +單一標題範例: +【台灣出貨】新中式掛鐘 古典復古 簡約現代 繁體漢字 客廳餐廳 個性藝術創意 靜音時鐘 客廳裝飾 藝術時鐘 + +================================================================================ +================================================================================ +五、其他注意事項 +================================================================================ +• 相似商品批量生成時,修飾詞重複度需低於 50%(建議輪替同義詞) +• 所有標題必須使用繁體中文 +• 嚴格遵守字數限制(建議目標控制在 55~58 字符,留緩衝空間) + +================================================================================ +六、時效詞處理原則(重要補充) +================================================================================ +• 原標題「有」時效詞 → 新標題必須精準保留 +• 原標題「沒有」時效詞 → 新標題嚴禁添加任何時效承諾詞 + +================================================================================ +七、醫療與銀髮族商品特殊處理 +================================================================================ +嚴禁使用詞彙: +醫療、醫用、醫生推薦、臨床、治療、療效、復健、關節炎、靜脈曲張、腰椎間盤突出、止痛、消炎、殺菌、防褥瘡、降血壓、醫療級、復健車、血氧機、血壓計、助聽器、矯正帶 + +銀髮族/樂齡商品安全替代詞建議: +• 復健購物車 → 家用休具、休閒買菜車、輕便購物車 +• 矯正坐墊 → 減壓坐墊、人體工學靠枕、久坐舒適款 +• 按摩治療儀 → 居家舒壓按摩、放鬆神器、日常舒適好物 + +================================================================================ +八、符合台灣用戶搜索習慣的實戰優化建議 +================================================================================ +1. 核心關鍵詞選擇優先使用平台即時建議 + 在蝦皮搜尋欄輸入商品類型,觀察自動跳出的熱門詞,優先選用搜尋量高的詞放在「【台灣出貨】」之後第二位。 + +2. 利益導向優先 + 台灣用戶特別愛搜:顯瘦、遮肉、寬鬆舒適、親膚、透氣涼感、百搭、日常穿搭、高CP值、輕薄無負擔。 + 建議在核心關鍵詞後盡量放這些「為什麼要買」的利益詞。 + +3. 自然流暢可讀(手機優先) + 標題要像自然中文一樣好讀,手機上掃一眼就能懂。避免硬塞關鍵詞造成斷裂感。 + +4. 同義詞輪替策略(批量專用) + 透氣涼感 / 親膚舒適 / 輕薄透氣 / 抗熱消暑 / 舒適無負擔 + 顯瘦 / 遮肉顯瘦 / 寬鬆顯瘦 / 版型優良 + 這樣可大幅降低重複度,同時維持高相關性。 + +5. 強化使用場景詞 + 台灣用戶常搜「日常穿」「居家穿」「出門穿」「媽媽實用」「通勤穿搭」「夏季必備」。適度加入能提升點擊率。 + +6. 信任感已由「台灣出貨」建立 + 不需再堆疊「現貨」「快速到貨」。可視情況輕加「品質嚴選」「台灣設計風格」(若原標題有相關元素)。 +7. 字數精準控制技巧 + 建議目標控制在 55~58 字符,留一點緩衝空間。計算時務必包含空格與【】。 + +8. 服飾類風格精準動態匹配 + - 辣妹/純欲風:設計感、性感氣質、顯瘦曲線 + - 文藝復古/媽媽裝:復古知性、寬鬆顯瘦、實用舒適 + - 法式/名媛風:優雅高質感、知性氣質 + +9. 定期迭代與數據驗證 + 上架後觀察賣家後台「曝光次數」與「點擊率」,把表現好的熱門關鍵詞保留或加強,表現差的替換。 + +10. 避免平台風險 + 嚴格遵守「原標題沒有就不要加」的原則(材質、尺寸、醫療詞),這是長期穩定排名的關鍵。 + +================================================================================ +輸出範例 +================================================================================ +單一標題範例: +【台灣出貨】新中式掛鐘 古典復古 簡約現代 繁體漢字 客廳餐廳 個性藝術創意 靜音時鐘 客廳裝飾 藝術時鐘 + +================================================================================ +注意事項總結 +================================================================================ +• 嚴格遵守字數 52-60 字符 +• 繁體中文輸出 +• 保留原標題風格詞與版型詞 +• 時效詞「有就留、沒有就絕不加」 +• 材質與尺寸「原標題沒有就絕不加」 +• 醫療相關詞一律禁用,銀髮族改用安全替代詞 +• 批量時修飾詞重複度低於 50% +• 建議定期根據賣家後台數據迭代熱門關鍵詞 \ No newline at end of file diff --git a/app/gui/tabs/generate.py b/app/gui/tabs/generate.py index 906e8a0..3f4386f 100644 --- a/app/gui/tabs/generate.py +++ b/app/gui/tabs/generate.py @@ -764,6 +764,7 @@ class GenerateTab(QWidget): self.open_accounts_callback = open_accounts_callback self.title_prompt_path = title_prompt_path or appconfig.title_prompt_path(self.config) self.cover_prompts_dir = cover_prompts_dir or appconfig.cover_prompts_dir(self.config) + prompts.ensure_default_prompts(self.title_prompt_path, self.cover_prompts_dir) self.current_cover_template = None self.generate_worker = None self.generate_thread = None diff --git a/app/prompts.py b/app/prompts.py index cec46f0..915bb7d 100644 --- a/app/prompts.py +++ b/app/prompts.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +from importlib import resources from . import appconfig @@ -10,6 +11,8 @@ TITLE_PROMPT_PATH = appconfig.title_prompt_path() COVER_PROMPTS_DIR = appconfig.cover_prompts_dir() TEMPLATE_EXT = ".txt" INVALID_NAME_CHARS = set('\\/:*?"<>|') +DEFAULT_PROMPTS_PACKAGE = "app.default_prompts" +DEFAULT_COVER_PROMPTS_PACKAGE = "app.default_prompts.cover" class PromptError(RuntimeError): @@ -35,6 +38,27 @@ def save_title_prompt(text, path=TITLE_PROMPT_PATH) -> None: fh.write(str(text or "")) +def ensure_default_prompts( + title_prompt_path=TITLE_PROMPT_PATH, + cover_prompts_dir=COVER_PROMPTS_DIR, +) -> None: + """Seed bundled default prompts into an empty user data directory. + + Existing user prompts are never overwritten. Defaults live in the packaged + app and are copied into data/ only when the corresponding user files are + missing, or the title prompt file is blank. + """ + + if not _has_non_empty_file(title_prompt_path): + default_title = _read_default_prompt_text("title_prompt.txt") + if default_title: + save_title_prompt(default_title, title_prompt_path) + + if not list_cover_templates(cover_prompts_dir): + for name, text in _iter_default_cover_templates(): + save_cover_template(name, text, cover_prompts_dir) + + def list_cover_templates(directory=COVER_PROMPTS_DIR): """Return cover template names sorted by display name.""" @@ -132,3 +156,40 @@ def _normalize_name(name) -> str: if os.path.basename(value) != value: raise PromptError(f"封面提示词模板名非法: {value}") return value + + +def _has_non_empty_file(path) -> bool: + try: + if not os.path.exists(path): + return False + with open(path, "r", encoding="utf-8") as fh: + return bool(fh.read().strip()) + except OSError: + return False + + +def _read_default_prompt_text(filename) -> str: + try: + return ( + resources.files(DEFAULT_PROMPTS_PACKAGE) + .joinpath(filename) + .read_text(encoding="utf-8") + ) + except (FileNotFoundError, ModuleNotFoundError, OSError): + return "" + + +def _iter_default_cover_templates(): + try: + root = resources.files(DEFAULT_COVER_PROMPTS_PACKAGE) + except (ModuleNotFoundError, OSError): + return + for entry in root.iterdir(): + if not entry.is_file() or not entry.name.lower().endswith(TEMPLATE_EXT): + continue + name = entry.name[: -len(TEMPLATE_EXT)] + try: + text = entry.read_text(encoding="utf-8") + except OSError: + continue + yield name, text diff --git a/cmshopee.spec b/cmshopee.spec index 56cdaec..b7b1e7a 100644 --- a/cmshopee.spec +++ b/cmshopee.spec @@ -7,13 +7,19 @@ include config files, SQLite databases, Chrome profiles, generated images, logs, prompts, or other operator data. """ +from PyInstaller.utils.hooks import collect_data_files + block_cipher = None +default_prompt_datas = collect_data_files( + "app.default_prompts", + includes=["**/*.txt"], +) a = Analysis( ["main.py"], pathex=[], binaries=[], - datas=[], + datas=default_prompt_datas, hiddenimports=[ "PySide6.QtCore", "PySide6.QtGui", diff --git a/docs/tasks/T-579.md b/docs/tasks/T-579.md index 74b6bad..3185b55 100644 --- a/docs/tasks/T-579.md +++ b/docs/tasks/T-579.md @@ -3,7 +3,7 @@ id: T-579 title: 打包版内置默认标题和封面提示词,首次运行自动写入用户 data phase: 8 deps: [T-524, T-538, T-540] -status: TODO +status: DONE created: 2026-07-10 --- @@ -60,3 +60,13 @@ created: 2026-07-10 - 不提交 `data/config.json`、`data/config/cmhub.json`、`data/cmshopee.db`、`data/images/`、`data/chrome_user_data_dir/`、`data/logs/`。 - 不改 AI/cmhub 请求协议、生成并发、DB schema、Excel、Shopee/CDP 更新流程。 - 不在升级时覆盖用户已经修改过的提示词。 + +## 执行记录 + +- 2026-07-10:完成 T-579。 + - 新增 `app/default_prompts/` 默认提示词资源包,内置当前已验证使用的标题提示词和封面模板 `papa1`;发布前已扫描常见密钥关键词,未发现 API Key、Cookie、token、密码等密钥类内容。 + - `app/prompts.py` 新增 `ensure_default_prompts()`:用户 `data/title_prompt.txt` 缺失或为空时写入默认标题提示词;用户 `data/prompts/cover/` 没有模板时写入默认封面模板;已有用户提示词或模板时不覆盖。 + - `app/gui/tabs/generate.py` 在②AI生成页读取提示词前执行默认提示词初始化,保证全新用户首次打开即可看到默认标题提示词和 `papa1` 封面模板。 + - `cmshopee.spec` 改为通过 `collect_data_files("app.default_prompts")` 收集内置默认提示词文本;仍不打包 release 根目录的 `data/`、`title_prompt.txt`、`prompts/`、DB、配置、图片、日志或登录态。 + - 测试:`tests/test_prompts.py` 覆盖默认提示词播种和不覆盖用户提示词;`tests/test_gui.py` 覆盖②AI生成页全新用户自动回显默认提示词;`tests/test_packaging.py` 覆盖 PyInstaller spec 收集默认资源。 + - 验证通过:`py -3.10 -m unittest tests.test_prompts tests.test_gui tests.test_packaging`、`python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`py -3.10 -m unittest discover -s tests`(319 tests)、`git diff --check`。 diff --git a/tests/test_gui.py b/tests/test_gui.py index 16e052d..33b7688 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -1857,7 +1857,9 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assertEqual(0, tab.cover_progress_bar.value()) self.assertEqual(1, tab.title_progress_bar.maximum()) self.assertEqual(1, tab.cover_progress_bar.maximum()) - self.assertEqual("默认", tab.cover_template_combo.currentText()) + self.assertIn("蝦皮台灣站", tab.title_prompt_edit.toPlainText()) + self.assertEqual("papa1", tab.cover_template_combo.currentText()) + self.assertIn("商品标题:{新标题}", tab.cover_prompt_edit.toPlainText()) self.assertEqual("模板操作", tab.cover_template_actions_button.text()) self.assertIs(tab.cover_template_actions_menu, tab.cover_template_actions_button.menu()) self.assertEqual( @@ -2234,6 +2236,27 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assert_removed(temp_dir) + def test_generate_tab_seeds_default_prompts_for_new_user_data(self): + with self.make_temp_dir() as temp_dir: + cfg = self.make_config(temp_dir) + title_prompt_path = os.path.join(temp_dir, "title_prompt.txt") + cover_prompts_dir = os.path.join(temp_dir, "prompts", "cover") + + tab = GenerateTab( + config=cfg, + title_prompt_path=title_prompt_path, + cover_prompts_dir=cover_prompts_dir, + ) + self.addCleanup(tab.close) + + self.assertIn("蝦皮台灣站", tab.title_prompt_edit.toPlainText()) + self.assertEqual("papa1", tab.cover_template_combo.currentText()) + self.assertIn("商品标题:{新标题}", tab.cover_prompt_edit.toPlainText()) + self.assertTrue(os.path.exists(title_prompt_path)) + self.assertTrue(os.path.exists(os.path.join(cover_prompts_dir, "papa1.txt"))) + + self.assert_removed(temp_dir) + def test_generate_tab_manages_prompt_files_and_preview(self): with self.make_temp_dir() as temp_dir: cfg = self.make_config(temp_dir) diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 2000c58..f527720 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -21,7 +21,10 @@ class PackagingTests(unittest.TestCase): spec = self.read_text("cmshopee.spec") normalized = "".join(spec.split()) - self.assertIn("datas=[]", normalized) + self.assertIn("collect_data_files", spec) + self.assertIn('"app.default_prompts"', spec) + self.assertIn('includes=["**/*.txt"]', normalized) + self.assertIn("datas=default_prompt_datas", normalized) self.assertIn("COLLECT(", spec) self.assertIn('name="cmshopee"', normalized) diff --git a/tests/test_prompts.py b/tests/test_prompts.py index eb3a41d..b9096bc 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -45,6 +45,40 @@ class PromptTests(TempDirMixin, unittest.TestCase): self.assert_removed(temp_dir) + def test_ensure_default_prompts_seeds_empty_user_prompt_files(self): + with self.make_temp_dir() as temp_dir: + title_path = os.path.join(temp_dir, "title_prompt.txt") + cover_dir = os.path.join(temp_dir, "prompts", "cover") + + prompts.ensure_default_prompts(title_path, cover_dir) + + self.assertIn("蝦皮台灣站", prompts.load_title_prompt(title_path)) + self.assertEqual(["papa1"], prompts.list_cover_templates(cover_dir)) + self.assertIn( + "商品标题:{新标题}", + prompts.load_cover_template("papa1", cover_dir), + ) + + self.assert_removed(temp_dir) + + def test_ensure_default_prompts_does_not_overwrite_user_prompts(self): + with self.make_temp_dir() as temp_dir: + title_path = os.path.join(temp_dir, "title_prompt.txt") + cover_dir = os.path.join(temp_dir, "prompts", "cover") + prompts.save_title_prompt("用户标题提示词", title_path) + prompts.save_cover_template("用户模板", "用户封面提示词", cover_dir) + + prompts.ensure_default_prompts(title_path, cover_dir) + + self.assertEqual("用户标题提示词", prompts.load_title_prompt(title_path)) + self.assertEqual(["用户模板"], prompts.list_cover_templates(cover_dir)) + self.assertEqual( + "用户封面提示词", + prompts.load_cover_template("用户模板", cover_dir), + ) + + self.assert_removed(temp_dir) + def test_render_prompt_replaces_known_variables(self): task = { "old_title": "舊T恤",