163 lines
5.9 KiB
Python
163 lines
5.9 KiB
Python
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)
|