diff --git a/main_voc分析.py b/main_voc分析.py
index d7bb86b..0bdd7fa 100644
--- a/main_voc分析.py
+++ b/main_voc分析.py
@@ -4,7 +4,7 @@ VOC 全流程:合并 → 清洗 → 结构化 → 向量化 →(聚类 ∥
用法::
# 全流程(--industry 默认 Pet Supplies;写入 sqlite 前默认清理旧库,keep-db不清理)
- ./310py/bin/python main_voc分析.py --input-dir 目录 --product "产品名" --keep-db
+ ./310py/bin/python main_voc分析.py --input-dir '/Users/onesvmwhoops/Cursor_Project/VOC_LLM结构化/cat deterrent indoor ' --product "cat deterrent indoor" --keep-db
--industry "行业名"
# 断点续跑(步骤 4 起可省略 --product,自动读 voc_structured.sqlite)
./310py/bin/python main_voc分析.py --from-step 5
diff --git a/prompts/extraction/examples.yaml b/prompts/extraction/examples.yaml
index f31ceb3..5933340 100644
--- a/prompts/extraction/examples.yaml
+++ b/prompts/extraction/examples.yaml
@@ -52,7 +52,26 @@
sentiment: Positive
category: Price
-- instruction: "示例 4 教学:保留关键成分与症状等核心具体词汇。绝不能将带有具体成分/病症的词泛化提取。例如遇到「chicken flavor」(鸡肉风味)或「joint pain」(关节疼痛)时,必须保留核心修饰词,绝不能错误缩减提取为「flavor」或「pain」。"
+- instruction: "示例 4 教学:非英语评论(例如西班牙语)输出必须全英文。禁止保留原文非英文片段,先理解语义再用英文短语表达。"
+ review: "El producto en sí está muy bien, viene con 2 cristales templados y 2 grips para los joycon. Pero es lo que he leido en algunos comentarios, la funda huele como a tabaco jajaja es raro pero es así"
+ output:
+ audience: "self"
+ pain_points: []
+ product_feedback:
+ - aspect: "product"
+ opinion: "it's fine"
+ sentiment: Positive
+ category: Quality
+ - aspect: "included accessories"
+ opinion: "comes with 2 tempered glass and 2 grips"
+ sentiment: Positive
+ category: Function
+ - aspect: "case smell"
+ opinion: "smells like tobacco, strange"
+ sentiment: Negative
+ category: Quality
+
+- instruction: "示例 5 教学:保留关键成分与症状等核心具体词汇。绝不能将带有具体成分/病症的词泛化提取。例如遇到「chicken flavor」(鸡肉风味)或「joint pain」(关节疼痛)时,必须保留核心修饰词,绝不能错误缩减提取为「flavor」或「pain」。"
review: "My elderly cat suffers from severe joint pain. But he is very attracted by the chicken flavor of this supplement! It really helps him walk better."
output:
audience: "elderly cat"
diff --git a/prompts/extraction/field_rules.md b/prompts/extraction/field_rules.md
index 2fac9a7..7492212 100644
--- a/prompts/extraction/field_rules.md
+++ b/prompts/extraction/field_rules.md
@@ -1,3 +1,8 @@
+## 语言规则(红线):
+- 评论输入可能是英语、西班牙语、法语、德语、日语等**任意语言**。
+- **所有输出字段值**(audience、pain_points 每一条、aspect、opinion)**必须是自然英文**。
+- **禁止**保留原文非英文片段(如 está muy bien、huele como a tabaco、très bon);先理解语义,再用英文短语表达。
+
## 分析要求:
1. audience (为谁购买):
diff --git a/prompts/extraction/user_batch.md b/prompts/extraction/user_batch.md
index 848c38f..e240876 100644
--- a/prompts/extraction/user_batch.md
+++ b/prompts/extraction/user_batch.md
@@ -1,3 +1,3 @@
-以下为 {n_keys} 段带前缀的英文评论,每段互相独立。请严格按 system 中的「批量输出格式」返回 JSON,顶层键为 {keys_literal}。
+以下为 {n_keys} 段带前缀的用户评论(可能是任意语言),每段互相独立。所有输出字段值必须为英文。请严格按 system 中的「批量输出格式」返回 JSON,顶层键为 {keys_literal}。
{tagged_input}
diff --git a/prompts/loader.py b/prompts/loader.py
index eeeaf7a..953fb8d 100644
--- a/prompts/loader.py
+++ b/prompts/loader.py
@@ -161,7 +161,8 @@ def _report_marker_kwargs(
"marker_word_zh": markers[0],
"marker_word_category": markers[1],
"marker_cluster_names": markers[2],
- "marker_report_html": markers[3],
+ "marker_opportunity_matrix": markers[3],
+ "marker_report_html": markers[4],
"wordcloud_top_n": wf.get("wordcloud_top_n", 180),
"word_category_classify_n": wf.get("word_category_classify_n", 180),
"outlier_label_zh": rp.get("outlier_label_zh", "未归类"),
@@ -283,7 +284,7 @@ def validate_prompt_files() -> List[str]:
sch = get_schema()
if not word_categories(cfg):
errors.append("config.yaml: word_categories 为空")
- if len(report_markers(sch)) != 4:
+ if len(report_markers(sch)) != 5:
errors.append("schema.yaml: report.markers 须为 4 项")
if not product_feedback_categories(sch):
errors.append("schema.yaml: extraction.product_feedback_categories 为空")
diff --git a/prompts/report/analysis_requirements.md b/prompts/report/analysis_requirements.md
index 5b29c82..177d385 100644
--- a/prompts/report/analysis_requirements.md
+++ b/prompts/report/analysis_requirements.md
@@ -5,4 +5,14 @@
2.分受众分析 — 对 top2 受众分别写(1)用户需求(2)正面(3)负面(4)客观(仅用对应 2a/2b stage)。
- **(二)全部用户需求与产品反馈**:1.全部受众需求(3a)、2.正面、3.负面、4.客观(3b 三档);
每个
只收纳对应 stage 的簇;占比=结构化短语数÷a;同 ul 内按短语数降序。正文禁用「观点 / 评价 / 痛点」。
-3. **三、改进建议与机会**:保持模版 与四条 结构;建议须可执行,第 4 条单独写可放大的产品/市场机会;覆盖未满足需求、负面反馈与客观描述中的风险,勿复述本条款文字。
+ - **(三)多维交叉洞察**(新增):从聚类数据中推断以下维度并进行交叉分析:
+ 1.**使用场景推断** — 从评论关键词和需求簇推断用户的典型使用场景(如日常家用、专业工作、礼品/转售、特定季节等),分析不同场景下的需求差异。
+ 2.**购买动机分析** — 推断驱动购买的核心动机(功能刚需、性价比、品牌信任、替代竞品、尝新等),每个动机引用具体聚类数据支撑。
+ 3.**人群×需求矩阵** — 将 top2 受众与 top 需求/反馈做交叉比对,找出各群体的独有关注点(A群体在意 X 但 B群体不在意)和共性盲区。
+ 每条洞察必须引用具体数据占比或簇名,禁止空泛概括。
+3. **三、改进建议与机会**:须严格按模版结构输出六条 - 。
+ - 前三条为**短期快赢**建议(0-3月可落地),包括产品本身的微调、包装/说明书优化、Listing与A+页面优化。
+ - 第四条为**中期优化**(3-6月),涵盖配件组合/套装策略、服务升级(售后/质保/教程内容)、差异化定价与变体策略。
+ - 第五条为**长期布局**(6-12月),覆盖新品类延伸、场景化产品线、品牌社群与内容生态等前瞻性方向。
+ - 第六条为**差异化机会**,专门指出竞品未覆盖但本品有潜力占领的空白市场/场景/人群机会。
+ - 每条建议须关联具体数据洞察(引用对应簇名或占比),不可泛泛而谈。
diff --git a/prompts/report/correction.md b/prompts/report/correction.md
index cf3a283..9d36c0f 100644
--- a/prompts/report/correction.md
+++ b/prompts/report/correction.md
@@ -4,7 +4,7 @@
{err_block}
【修正要点】
-- 四个标记必须按顺序出现:{marker_word_zh}、{marker_word_category}、{marker_cluster_names}、{marker_report_html}(JSON 在前,HTML 最后)
+- 五个标记必须按顺序出现:{marker_word_zh}、{marker_word_category}、{marker_cluster_names}、{marker_opportunity_matrix}、{marker_report_html}(JSON 在前,HTML 最后)
- JSON 段必须是严格合法 JSON:键与字符串均用英文双引号;例如 "words":["dog","cat"],禁止写成 "words:["dog"] 或 words:[
- 不要用 Markdown 代码围栏包裹 JSON;WORD_CATEGORY_JSON 的键仅限:{cats_literal}
- WORD_CATEGORY_JSON 必须是**一个** JSON 对象,words 仅来自 Top{word_category_classify_n};逐词尽量归类、不强制每类凑满;禁止「其他」键;禁止多段 `{{"某类":[...]}}, "另一类":[...]`
diff --git a/prompts/report/json_markers.md b/prompts/report/json_markers.md
index 04e0df1..86dc1e7 100644
--- a/prompts/report/json_markers.md
+++ b/prompts/report/json_markers.md
@@ -1,4 +1,4 @@
-请严格按以下四段标记输出(标记外不要有任何文字;**顺序不可调换**):
+请严格按以下五段标记输出(标记外不要有任何文字;**顺序不可调换**):
{marker_word_zh}
一行合法 JSON 对象:Top{wordcloud_top_n} 英文词 -> 中文,如 {{"dog":"狗","cat":"猫"}}。
@@ -19,6 +19,18 @@
簇标签 -1 统一命名为「{outlier_label_zh}」。
命名规则:1_audience、3a_pain_global、3b_aspect_opinion_* 写完整业务簇名;
2a/2b 只写子主题(程序拼接受众名);2b 子主题禁止含情感词。
+【簇名风格红线】每个簇名最多 15 个中文字,必须用日常口语,让运营人员一眼看懂。
+ 正确示范:「多猫家庭」「喷头漏液」「猫在床上乱尿」「气味刺鼻」「完全无效」
+ 错误示范:「针对床铺沙发等亲密区域的定点排尿抗议行为」「效力根本性不足导致乱尿行为完全无法被中断」
+
+{marker_opportunity_matrix}
+一个合法 JSON 数组,恰好 6 个元素,与 REPORT_HTML 中「三、改进建议与机会」的 6 条
- 一一对应。
+每个元素格式为:
+{{"title":"策略标题(≤15字)","phase":"short|mid|long|diff","impact":1-10,"effort":1-10,"data_ref":"引用的核心数据点(如'猫在床上乱尿 15.5%')"}}
+- phase 取值:short=短期快赢,mid=中期优化,long=长期布局,diff=差异化机会
+- impact=预期业务影响力(10=最大),effort=实施难度(10=最难)
+- data_ref 须引用报告中具体的簇名或占比数据
+示例:[{{"title":"升级缓释配方","phase":"short","impact":9,"effort":5,"data_ref":"效果仅持续数小时 6.8%"}}, ...]
{marker_report_html}
(最后输出)完整 HTML 文档,结构遵循上文模版;勿用 Markdown 代码围栏包裹。
diff --git a/prompts/report/output_format.md b/prompts/report/output_format.md
index ee009b9..b3800e9 100644
--- a/prompts/report/output_format.md
+++ b/prompts/report/output_format.md
@@ -1,8 +1,11 @@
**输出格式(必须严格遵守):**
1. 只允许输出标准 HTML,不允许 Markdown。
2. {marker_report_html} 内必须是完整 HTML 文档,包含 、、;可保留模版中的 ,浏览器不会显示。
-3. 允许标签:
- ;列表项格式须为 纯中文簇名 (XX.X%):洞察。
+3. 允许标签:
- ;列表项格式须为 纯中文簇名 (XX.X%):洞察。簇名必须控制在 15 字以内,用日常口语概括,禁止学术化长句。
4. 禁止 Markdown(#、**、``` 等)及
等未列出的标签。
+4b. 【文风红线】洞察描述必须直白务实,每句只说一件事。禁止文学性夸张修辞(如"切肤之痛""灾难性崩塌""谋杀式""奇迹心理""令人愉悦"等)。保持客观商业语气。
5. 【红线】正文可见文字中禁止出现 1_audience|2、2a_pain_audience_c2|0、stage=、audience_c 等任何机器标识;仅 CLUSTER_NAMES JSON 键可保留原始 id。
6. 不要输出解释性前后缀;REPORT_HTML 段内只放 HTML 文档本体。
-7. 四段标记顺序固定:先三个 JSON 段(WORD_ZH → WORD_CATEGORY → CLUSTER_NAMES),**最后**输出 REPORT_HTML,避免长 HTML 导致 JSON 被截断。
+7. 五段标记顺序固定:先四个 JSON 段(WORD_ZH → WORD_CATEGORY → CLUSTER_NAMES → OPPORTUNITY_MATRIX),**最后**输出 REPORT_HTML,避免长 HTML 导致 JSON 被截断。
+8. **(三)多维交叉洞察**的三个子板块各至少包含 3 条 - 分析;每条
- 必须以 洞察标题 开头并引用数据占比。
+9. **三、改进建议与机会**的
必须恰好 6 条 - ,分别对应短期快赢(3条)、中期优化(1条)、长期布局(1条)、差异化机会(1条),每条以 策略标题: 开头。
diff --git a/prompts/report/system.md b/prompts/report/system.md
index 9cb3c67..0047c15 100644
--- a/prompts/report/system.md
+++ b/prompts/report/system.md
@@ -2,3 +2,17 @@
报告必须专业、可读,正文中绝对禁止出现原始聚类 ID、stage 代码名或簇标签编号。
展示用语统一:用户需求、产品反馈、产品客观描述;禁止观点、评价、痛点等旧称。
你必须严格遵守 HTML 输出规范,禁止 Markdown。
+
+【语言风格——红线要求】
+你的读者是业务团队和高管,不是学术论文审稿人。务必:
+- 用大白话写作,一句话只传达一个核心信息,避免多层从句嵌套。
+- 簇名和标题限制在 15 个中文字以内,用最日常的语言概括核心要点。
+ 正确示范:「猫在床/沙发上乱尿」「产品气味太刺鼻」「喷头漏液」「产品完全无效」
+ 错误示范:「针对床铺沙发等亲密区域的定点排尿抗议行为」「效力根本性不足导致乱尿行为完全无法被中断」
+- 描述要具体但不要堆砌修辞——说清"是什么、有多严重、影响谁"即可,删掉所有文学性修饰(如"切肤之痛""灾难性崩塌""谋杀式破坏")。
+- 如果原始评论用了生动的英文原词,可以括号引用(如"stopped peeing"),但不要自行发明夸张措辞。
+
+你的核心分析原则:
+1. **多维交叉验证**:不仅分析"用户说了什么",还要推断其背后的使用场景、用户人群特征、购买动机,并交叉比对不同维度的数据,发现隐藏洞察。
+2. **机会多元化**:改进方案绝不局限于现有产品本身的修改——应覆盖服务升级、配件组合、场景化套装、全新品类延伸、内容与社群运营等多条路径。方案之间要体现差异化梯度(短期快赢 → 中期优化 → 长期布局)。
+3. **逻辑递进**:报告整体遵循「数据 → 洞察 → 机会 → 方案」的叙事线,每个结论都能回溯到具体数据支撑。
diff --git a/prompts/schema.yaml b/prompts/schema.yaml
index 9795be0..cbe593a 100644
--- a/prompts/schema.yaml
+++ b/prompts/schema.yaml
@@ -28,4 +28,5 @@ report:
- "===WORD_ZH_JSON==="
- "===WORD_CATEGORY_JSON==="
- "===CLUSTER_NAMES_JSON==="
+ - "===OPPORTUNITY_MATRIX_JSON==="
- "===REPORT_HTML==="
diff --git a/voc_report.py b/voc_report.py
index 8553060..2f9d7d5 100644
--- a/voc_report.py
+++ b/voc_report.py
@@ -119,10 +119,10 @@ CATEGORY_COLORS: Dict[str, str] = {
WORDCLOUD_UNCATEGORIZED_COLOR = "#9ca3af"
TRANSLATE_CHARS_PER_REQUEST = 200_000
-TRANSLATE_MAX_OUTPUT_TOKENS = 65536
-REPORT_MAX_OUTPUT_TOKENS = 65536
+TRANSLATE_MAX_OUTPUT_TOKENS = 200000
+REPORT_MAX_OUTPUT_TOKENS = 200000
TRANSLATE_LLM_TIMEOUT_SEC = 600.0
-REPORT_LLM_TIMEOUT_SEC = 600.0
+REPORT_LLM_TIMEOUT_SEC = 720.0
REPORT_PARSE_MAX_RETRIES = 2
# JSON 段放前、HTML 放后:输出被 max_tokens 截断时优先保留可解析的 JSON
REPORT_MARKERS: Tuple[str, ...] = (
@@ -510,10 +510,16 @@ def build_cluster_bundles(
stage_ratio = cnt / stage_total if stage_total else 0.0
ratio = stage_ratio
ratio_global = phrase_cnt / total_reviews if total_reviews else 0.0
+ skip_filter = (
+ stage == STAGE_1_AUDIENCE
+ or stage.startswith(STAGE_2A_PAIN_PREFIX)
+ or stage == STAGE_3A_PAIN
+ )
if (
min_cluster_review_ratio is not None
and stage_total > 0
and stage_ratio < min_cluster_review_ratio
+ and not skip_filter
):
logger.info(
"报告跳过小簇 %s / %s:%s 条评论 (本步骤 %.1f%% < %.0f%%)",
@@ -977,10 +983,11 @@ def _build_report_prompt(
word_freq: List[Tuple[str, int]],
top2_audience_clusters: Sequence[int],
) -> Tuple[str, str]:
- wf_table = word_freq[:WORD_FREQ_TABLE_N]
- wf_classify = word_freq[:WORD_CATEGORY_CLASSIFY_N]
- wf_table_lines = "\n".join(f"{w}\t{c}" for w, c in wf_table)
- wf_classify_lines = "\n".join(f"{w}\t{c}" for w, c in wf_classify)
+ n_table = WORD_FREQ_TABLE_N
+ n_classify = WORD_CATEGORY_CLASSIFY_N
+ n_max = max(n_table, n_classify)
+ wf_max = word_freq[:n_max]
+ wf_lines = "\n".join(f"{w}\t{c}" for w, c in wf_max)
cats_literal = "、".join(WORD_CATEGORIES)
stage_map = _cluster_stage_mapping_guide(
bundles, top2_audience_clusters=top2_audience_clusters
@@ -992,16 +999,23 @@ def _build_report_prompt(
top2_audience_clusters=top2_audience_clusters,
)
system = build_report_system()
+ if n_table == n_classify:
+ wf_block = (
+ f"【词频 Top{n_max}(word\\tcount,用于辅助撰写报告 + WORD_CATEGORY_JSON 分类,排名 1-{n_max})】\n"
+ f"{wf_lines}"
+ )
+ else:
+ wf_block = (
+ f"【词频 Top{n_max}(word\\tcount,排名 1-{n_max})】\n"
+ f"前 {n_table} 行用于辅助理解品类与撰写报告;前 {n_classify} 行**仅**用于 WORD_CATEGORY_JSON。\n"
+ f"{wf_lines}"
+ )
user = f"""请基于以下用户评论反馈的聚类结果,撰写一份详细的《{product_name}》产品改进建议报告。
行业背景:{industry}
数据清洗后有效评论总数 a = {total_reviews}
-【词频 Top{WORD_FREQ_TABLE_N}(word\\tcount,辅助理解品类与撰写报告,排名 1-{WORD_FREQ_TABLE_N})】
-{wf_table_lines}
-
-【词频分类词表 Top{WORD_CATEGORY_CLASSIFY_N}(word\\tcount,**仅**用于 WORD_CATEGORY_JSON,排名 1-{WORD_CATEGORY_CLASSIFY_N})】
-{wf_classify_lines}
+{wf_block}
【聚类流程与报告小节映射】
{stage_map}
@@ -1878,6 +1892,92 @@ def _build_word_category_map(category_data: Dict[str, Any]) -> Dict[str, List[st
return dict(by_word)
+def _audience_pie_data(bundles: List[ClusterBundle]) -> List[dict]:
+ """受众饼图:1_audience 阶段各簇的去重评论数。"""
+ return [
+ {"name": b.cluster_title_zh, "value": b.review_count}
+ for b in bundles
+ if b.stage == "1_audience"
+ ]
+
+
+def _sentiment_bar_data(bundles: List[ClusterBundle]) -> Dict[str, int]:
+ """情感分布柱状图:3b 阶段按正/负/中汇总结构化短语数。"""
+ agg: Dict[str, int] = {"positive": 0, "negative": 0, "neutral": 0}
+ for b in bundles:
+ for key in agg:
+ if b.stage == f"3b_aspect_opinion_{key}":
+ agg[key] += b.phrase_count
+ return agg
+
+
+def _pain_top_chart_data(
+ bundles: List[ClusterBundle], *, top_n: int = 10
+) -> List[dict]:
+ """全量需求 Top-N 条形图:3a_pain_global 阶段各簇短语数。"""
+ subset = [
+ b for b in bundles
+ if b.stage == STAGE_3A_PAIN and b.cluster_label != -1
+ ]
+ subset.sort(key=lambda b: -b.phrase_count)
+ return [
+ {"name": b.cluster_title_zh, "value": b.phrase_count}
+ for b in subset[:top_n]
+ ]
+
+
+def _negative_top_chart_data(
+ bundles: List[ClusterBundle], *, top_n: int = 10
+) -> List[dict]:
+ """负面反馈 Top-N 条形图。"""
+ subset = [
+ b for b in bundles
+ if b.stage == STAGE_3B_NEGATIVE and b.cluster_label != -1
+ ]
+ subset.sort(key=lambda b: -b.phrase_count)
+ return [
+ {"name": b.cluster_title_zh, "value": b.phrase_count}
+ for b in subset[:top_n]
+ ]
+
+
+def _radar_chart_data(
+ category_data: Dict[str, Any],
+ word_freq: List[Tuple[str, int]],
+) -> List[dict]:
+ """词频分类雷达图:各类别词频总和。"""
+ count_map = {w.lower(): c for w, c in word_freq}
+ result: List[dict] = []
+ for cat in WORD_CATEGORIES:
+ block = category_data.get(cat, {})
+ words = block.get("words", []) if isinstance(block, dict) else []
+ total = sum(count_map.get(str(w).lower(), 0) for w in words)
+ result.append({"name": cat, "value": total})
+ return result
+
+
+def _compute_dashboard_kpis(
+ total_reviews: int,
+ bundles: List[ClusterBundle],
+) -> Dict[str, Any]:
+ """Dashboard 四张 KPI 卡片的数据。"""
+ audience_count = sum(
+ 1 for b in bundles
+ if b.stage == "1_audience" and b.cluster_label != -1
+ )
+ sent = _sentiment_bar_data(bundles)
+ total_sent = sum(sent.values()) or 1
+ cluster_count = sum(1 for b in bundles if b.cluster_label != -1)
+ return {
+ "total_reviews": total_reviews,
+ "audience_count": audience_count,
+ "positive_ratio": round(sent["positive"] / total_sent * 100, 1),
+ "negative_ratio": round(sent["negative"] / total_sent * 100, 1),
+ "neutral_ratio": round(sent["neutral"] / total_sent * 100, 1),
+ "cluster_count": cluster_count,
+ }
+
+
def _wordcloud_color_for_categories(categories: List[str]) -> str:
if not categories:
return WORDCLOUD_UNCATEGORIZED_COLOR
@@ -2163,7 +2263,7 @@ def _render_ai_validation_section(
phrases_panel = _wrap_ai_verify_panel("聚类效果验证", phrases_html)
return (
'\n'
- "
AI分析效果验证
\n"
+ " 模型中间过程可视化
\n"
' 以下为结构化与聚类结果的抽样展示,点击标题展开查看。
\n'
f" {structured_panel}\n"
f" {phrases_panel}\n"
@@ -2179,20 +2279,37 @@ def _assemble_html(
phrases_html: str,
structured_html: str,
wordcloud_data: List[dict],
+ audience_pie_data: List[dict],
+ sentiment_data: Dict[str, int],
+ pain_top_data: List[dict],
+ negative_top_data: List[dict],
+ radar_data: List[dict],
+ kpis: Dict[str, Any],
) -> str:
- """按固定顺序机器拼装最终 HTML(不由 LLM 决定版块顺序)。"""
- data_json = json.dumps(wordcloud_data, ensure_ascii=False)
+ """按固定版块顺序拼装最终 HTML:Dashboard → 洞察报告 → 词频 → 附录。"""
page_title = f"{product_name} · 评论分析报告"
report_body = _strip_report_top_heading(report_html)
report_section = (
'\n'
- " 产品改进建议报告
\n"
f" {report_body}\n"
""
)
ai_verify_section = _render_ai_validation_section(structured_html, phrases_html)
- # 顺序固定:改进建议 → 词频 → AI 分析效果验证(内含结构化 / 聚类,可展开)
+
+ wc_json = json.dumps(wordcloud_data, ensure_ascii=False)
+ aud_json = json.dumps(audience_pie_data, ensure_ascii=False)
+ sent_json = json.dumps(sentiment_data, ensure_ascii=False)
+ pain_json = json.dumps(pain_top_data, ensure_ascii=False)
+ neg_json = json.dumps(negative_top_data, ensure_ascii=False)
+ radar_json = json.dumps(radar_data, ensure_ascii=False)
+
+ kpi_reviews = f"{kpis['total_reviews']:,}"
+ kpi_audiences = kpis["audience_count"]
+ kpi_pos = kpis["positive_ratio"]
+ kpi_neg = kpis["negative_ratio"]
+
body_sections = "\n".join((report_section, wordfreq_html, ai_verify_section))
+
return f"""
@@ -2202,9 +2319,48 @@ def _assemble_html(
- {page_title}
+
+
+
+
+
{page_title}
+
数据 \\u2192 洞察 \\u2192 机会 \\u2192 方案
+
+
+
+ 数据总览
+
+
+
{kpi_reviews}
+
有效评论数
+
+
+
{kpi_audiences}
+
受众群体
+
+
+
+
+
+
+
+
+
{body_sections}
-
+
+
+
+
"""
@@ -2416,6 +2749,14 @@ def generate_report(
cleaned_csv=cleaned_csv,
)
structured_sec = _render_structured_validation_section(struct_samples)
+
+ aud_pie = _audience_pie_data(bundles)
+ sent_bar = _sentiment_bar_data(bundles)
+ pain_top = _pain_top_chart_data(bundles, top_n=10)
+ neg_top = _negative_top_chart_data(bundles, top_n=10)
+ radar = _radar_chart_data(category_data, word_freq)
+ kpis = _compute_dashboard_kpis(total_reviews, bundles)
+
html = _assemble_html(
product_name=product_name,
wordfreq_html=wordfreq_sec,
@@ -2423,6 +2764,12 @@ def generate_report(
phrases_html=phrases_sec,
structured_html=structured_sec,
wordcloud_data=wc_data,
+ audience_pie_data=aud_pie,
+ sentiment_data=sent_bar,
+ pain_top_data=pain_top,
+ negative_top_data=neg_top,
+ radar_data=radar,
+ kpis=kpis,
)
output_html.parent.mkdir(parents=True, exist_ok=True)
output_html.write_text(html, encoding="utf-8")
diff --git a/结构化_server.py b/结构化_server.py
index 395446c..9a93e88 100644
--- a/结构化_server.py
+++ b/结构化_server.py
@@ -78,7 +78,7 @@ BATCH_COUNT_MIN = 1
# 动态分批时单批评论条数上限(避免单请求过大导致输出截断)
DEFAULT_MAX_BATCH_REVIEWS = 100
# Chat 批间并行;与 embedding 共用账号时不宜过高,避免连带 429
-STRUCT_DEFAULT_WORKERS = 8
+STRUCT_DEFAULT_WORKERS = 20
def _resolve_max_batch_reviews(explicit: int | None = None) -> int: