Files

168 lines
6.1 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, word_limit=limit)
except TypeError:
try:
summary = summary_provider(metrics, typical)
except Exception:
return DEFAULT_SUMMARY
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)