feat: 提交热榜评论分析工具 MVP 基线
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
import json
|
||||
from collections import Counter, defaultdict
|
||||
from collections.abc import Callable
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import Comment, ContentItem, Hotspot, Report
|
||||
|
||||
|
||||
DEFAULT_SUMMARY = "总结生成失败,请查看上方统计数据。"
|
||||
|
||||
|
||||
def _labels(value: str | None) -> list[str]:
|
||||
try:
|
||||
parsed = json.loads(value or "[]")
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
return [str(label) for label in parsed] if isinstance(parsed, list) else []
|
||||
|
||||
|
||||
def build_comment_metrics(comments: list[Comment]) -> dict:
|
||||
sentiment_counts = Counter(comment.sentiment or "unknown" for comment in comments)
|
||||
label_counts = Counter(label for comment in comments for label in _labels(comment.labels))
|
||||
total = len(comments)
|
||||
|
||||
def sentiment_entry(name: str) -> dict:
|
||||
count = sentiment_counts.get(name, 0)
|
||||
return {"count": count, "pct": round((count / total * 100) if total else 0, 2)}
|
||||
|
||||
return {
|
||||
"sample_count": total,
|
||||
"sentiment": {
|
||||
"positive": sentiment_entry("positive"),
|
||||
"negative": sentiment_entry("negative"),
|
||||
"neutral": sentiment_entry("neutral"),
|
||||
"unknown": sentiment_entry("unknown"),
|
||||
},
|
||||
"top_labels": [{"name": name, "count": count} for name, count in label_counts.most_common(5)],
|
||||
}
|
||||
|
||||
|
||||
def select_typical_comments(comments: list[Comment], *, per_sentiment: int = 2) -> dict[str, list[dict]]:
|
||||
buckets: dict[str, list[Comment]] = defaultdict(list)
|
||||
for comment in comments:
|
||||
buckets[comment.sentiment or "unknown"].append(comment)
|
||||
|
||||
result = {}
|
||||
for sentiment, values in buckets.items():
|
||||
sorted_values = sorted(values, key=lambda c: (c.like_count or 0, c.comment_time or c.created_at), reverse=True)
|
||||
result[sentiment] = [
|
||||
{"content": comment.content, "like_count": comment.like_count or 0, "comment_id": comment.source_comment_id}
|
||||
for comment in sorted_values[:per_sentiment]
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
def generate_item_report(
|
||||
session: Session,
|
||||
content_item_id: str,
|
||||
*,
|
||||
summary_provider: Callable[[dict, dict], str] | None = None,
|
||||
) -> Report:
|
||||
item = session.get(ContentItem, content_item_id)
|
||||
if item is None:
|
||||
raise ValueError("content item not found")
|
||||
hotspot = session.get(Hotspot, item.hotspot_id)
|
||||
comments = list(session.scalars(select(Comment).where(Comment.content_item_id == item.id)))
|
||||
metrics = build_comment_metrics(comments)
|
||||
typical = select_typical_comments(comments)
|
||||
summary = _safe_summary(summary_provider, metrics, typical, limit=200)
|
||||
markdown = _build_markdown(title=item.title or item.source_item_id, metrics=metrics, typical=typical, summary=summary, hotspot_title=hotspot.title if hotspot else "")
|
||||
report = Report(
|
||||
task_id=item.task_id,
|
||||
hotspot_id=item.hotspot_id,
|
||||
content_item_id=item.id,
|
||||
report_type="item",
|
||||
title=item.title or item.source_item_id,
|
||||
data=json.dumps(metrics, ensure_ascii=False),
|
||||
markdown=markdown,
|
||||
metrics_json=json.dumps(metrics, ensure_ascii=False),
|
||||
typical_comments_json=json.dumps(typical, ensure_ascii=False),
|
||||
summary=summary,
|
||||
markdown_content=markdown,
|
||||
)
|
||||
session.add(report)
|
||||
session.commit()
|
||||
session.refresh(report)
|
||||
return report
|
||||
|
||||
|
||||
def generate_hotspot_report(
|
||||
session: Session,
|
||||
hotspot_id: str,
|
||||
*,
|
||||
summary_provider: Callable[[dict, dict], str] | None = None,
|
||||
) -> Report:
|
||||
hotspot = session.get(Hotspot, hotspot_id)
|
||||
if hotspot is None:
|
||||
raise ValueError("hotspot not found")
|
||||
comments = list(session.scalars(select(Comment).where(Comment.hotspot_id == hotspot.id)))
|
||||
item_count = session.scalar(select(func.count(ContentItem.id)).where(ContentItem.hotspot_id == hotspot.id)) or 0
|
||||
metrics = build_comment_metrics(comments)
|
||||
metrics["item_count"] = item_count
|
||||
typical = select_typical_comments(comments)
|
||||
summary = _safe_summary(summary_provider, metrics, typical, limit=300)
|
||||
markdown = _build_markdown(title=hotspot.title, metrics=metrics, typical=typical, summary=summary)
|
||||
report = Report(
|
||||
task_id=hotspot.task_id,
|
||||
hotspot_id=hotspot.id,
|
||||
content_item_id=None,
|
||||
report_type="hotspot",
|
||||
title=hotspot.title,
|
||||
data=json.dumps(metrics, ensure_ascii=False),
|
||||
markdown=markdown,
|
||||
metrics_json=json.dumps(metrics, ensure_ascii=False),
|
||||
typical_comments_json=json.dumps(typical, ensure_ascii=False),
|
||||
summary=summary,
|
||||
markdown_content=markdown,
|
||||
)
|
||||
session.add(report)
|
||||
session.commit()
|
||||
session.refresh(report)
|
||||
return report
|
||||
|
||||
|
||||
def _safe_summary(summary_provider, metrics: dict, typical: dict, *, limit: int) -> str:
|
||||
if summary_provider is None:
|
||||
return DEFAULT_SUMMARY
|
||||
try:
|
||||
summary = summary_provider(metrics, typical)
|
||||
except Exception:
|
||||
return DEFAULT_SUMMARY
|
||||
return str(summary)[:limit]
|
||||
|
||||
|
||||
def _build_markdown(*, title: str, metrics: dict, typical: dict, summary: str, hotspot_title: str = "") -> str:
|
||||
lines = [
|
||||
f"# {title}",
|
||||
"",
|
||||
]
|
||||
if hotspot_title:
|
||||
lines.extend([f"- 所属热点:{hotspot_title}", ""])
|
||||
lines.extend(
|
||||
[
|
||||
f"- 样本评论数量:{metrics['sample_count']}",
|
||||
"",
|
||||
"## 情绪分布",
|
||||
]
|
||||
)
|
||||
for name in ("positive", "neutral", "negative", "unknown"):
|
||||
item = metrics["sentiment"][name]
|
||||
lines.append(f"- {name}: {item['count']} ({item['pct']}%)")
|
||||
lines.extend(["", "## Top 标签"])
|
||||
for label in metrics["top_labels"]:
|
||||
lines.append(f"- {label['name']}: {label['count']}")
|
||||
lines.extend(["", "## 典型评论"])
|
||||
for sentiment, comments in typical.items():
|
||||
for comment in comments:
|
||||
lines.append(f"- [{sentiment}] {comment['content']}")
|
||||
lines.extend(["", "## 总结", summary])
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user