Files
tougao/application/common/AiWritingRiskAnalysisService.php
2026-07-15 10:55:08 +08:00

231 lines
7.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common;
/**
* 编辑辅助AI 辅助写作风险综合分析
*/
class AiWritingRiskAnalysisService
{
/**
* @param array $parsedData parseManuscriptStructure 的 data 字段
*/
public function buildReport(array $parsedData): array
{
$templateStats = (array) ($parsedData['template_sentence_stats'] ?? []);
$repeatedStats = (array) ($parsedData['repeated_phrases'] ?? []);
$sentenceStats = (array) ($parsedData['sentence_stats'] ?? []);
$dimensions = [
$this->analyzeTemplateSentenceRisk($templateStats),
$this->analyzeRepeatedPhraseRisk($repeatedStats),
$this->analyzeSentenceLengthRisk($sentenceStats),
];
$overallScore = 0;
$levelRank = ['low' => 1, 'medium' => 2, 'high' => 3];
$maxRank = 1;
$flags = [];
foreach ($dimensions as $dimension) {
$overallScore += intval($dimension['score'] ?? 0);
$rank = $levelRank[$dimension['level'] ?? 'low'] ?? 1;
if ($rank > $maxRank) {
$maxRank = $rank;
}
foreach ((array) ($dimension['flags'] ?? []) as $flag) {
if ($flag !== '') {
$flags[] = $flag;
}
}
}
$overallLevel = array_search($maxRank, $levelRank, true) ?: 'low';
return [
'overall_level' => $overallLevel,
'overall_score' => min(100, $overallScore),
'summary' => $this->buildSummary($overallLevel, $dimensions, $flags),
'dimensions' => $dimensions,
'flags' => array_values(array_unique($flags)),
];
}
private function analyzeTemplateSentenceRisk(array $stats): array
{
$items = (array) ($stats['items'] ?? []);
$totalMatches = intval($stats['total_matches'] ?? 0);
$matchedRuleCount = intval($stats['matched_rule_count'] ?? 0);
$score = 0;
$flags = [];
foreach ($items as $item) {
$count = intval($item['count'] ?? 0);
$riskLevel = max(1, intval($item['risk_level'] ?? 1));
$weight = max(1, intval($item['weight'] ?? 1));
$score += $count * $riskLevel * $weight;
if ($count > 0) {
$section = (string) ($item['section'] ?? '');
$remark = trim((string) ($item['remark'] ?? ''));
$label = $remark !== '' ? $remark : (string) ($item['sentence_pattern'] ?? '');
$flags[] = sprintf('%s段命中模版句%s%d次', $section, $label, $count);
}
}
$level = $this->scoreToLevel($score, 15, 40);
return [
'key' => 'template_sentence',
'name' => '模版句特征',
'level' => $level,
'score' => min(100, $score),
'summary' => $totalMatches > 0
? sprintf('命中 %d 条模版句规则,共 %d 处', $matchedRuleCount, $totalMatches)
: '未检测到典型 AI 模版句',
'metrics' => [
'total_matches' => $totalMatches,
'matched_rule_count' => $matchedRuleCount,
],
'details' => $stats,
'flags' => array_slice($flags, 0, 8),
];
}
private function analyzeRepeatedPhraseRisk(array $stats): array
{
$items = (array) ($stats['suspicious_items'] ?? []);
if (empty($items)) {
$items = array_values(array_filter((array) ($stats['items'] ?? []), function ($item) {
return ($item['category'] ?? '') === 'suspicious';
}));
}
$uniqueCount = count($items);
$expectedCount = count((array) ($stats['expected_topic_items'] ?? []));
$topCount = 0;
$flags = [];
foreach (array_slice($items, 0, 5) as $item) {
$count = intval($item['count'] ?? 0);
if ($count > $topCount) {
$topCount = $count;
}
if ($count >= 4) {
$flags[] = sprintf('可疑重复短语「%s」出现 %d 次', (string) ($item['phrase'] ?? ''), $count);
}
}
$score = min(100, $uniqueCount * 3 + max(0, $topCount - 3) * 6);
$level = 'low';
if ($uniqueCount >= 10 || $topCount >= 8) {
$level = 'high';
} elseif ($uniqueCount >= 4 || $topCount >= 5) {
$level = 'medium';
}
$summary = '未发现可疑机械重复短语';
if ($uniqueCount > 0) {
$summary = sprintf('发现 %d 组可疑重复短语,最高重复 %d 次', $uniqueCount, $topCount);
} elseif ($expectedCount > 0) {
$summary = sprintf('主题词正常重复 %d 组,未触发可疑短语', $expectedCount);
}
return [
'key' => 'repeated_phrase',
'name' => '重复短语',
'level' => $level,
'score' => $score,
'summary' => $summary,
'metrics' => [
'suspicious_count' => $uniqueCount,
'expected_topic_count' => $expectedCount,
'top_count' => $topCount,
],
'details' => $stats,
'flags' => $flags,
];
}
private function analyzeSentenceLengthRisk(array $stats): array
{
$avg = floatval($stats['avg'] ?? 0);
$std = floatval($stats['std'] ?? 0);
$max = intval($stats['max'] ?? 0);
$sentenceCount = intval($stats['sentence_count'] ?? 0);
$score = 0;
$flags = [];
$level = 'low';
if ($sentenceCount >= 8) {
if ($avg >= 32) {
$score += 20;
$flags[] = sprintf('平均句长偏高(%.1f 词/句)', $avg);
}
if ($std > 0 && $std <= 5 && $avg >= 12 && $avg <= 28) {
$score += 25;
$flags[] = sprintf('句长分布过于均匀(均值 %.1f,标准差 %.1f', $avg, $std);
}
if ($max >= 50) {
$score += 10;
$flags[] = sprintf('存在超长句(最长 %d 词)', $max);
}
}
$level = $this->scoreToLevel($score, 10, 25);
return [
'key' => 'sentence_length',
'name' => '句长特征',
'level' => $level,
'score' => min(100, $score),
'summary' => $sentenceCount > 0
? sprintf('平均句长 %.1f,标准差 %.1f,共 %d 句', $avg, $std, $sentenceCount)
: '正文句长数据不足',
'metrics' => [
'avg' => $avg,
'std' => $std,
'max' => $max,
'min' => intval($stats['min'] ?? 0),
'sentence_count' => $sentenceCount,
],
'details' => $stats,
'flags' => $flags,
];
}
private function scoreToLevel(int $score, int $mediumThreshold, int $highThreshold): string
{
if ($score >= $highThreshold) {
return 'high';
}
if ($score >= $mediumThreshold) {
return 'medium';
}
return 'low';
}
private function buildSummary(string $overallLevel, array $dimensions, array $flags): string
{
$levelText = [
'low' => '低风险',
'medium' => '中等风险',
'high' => '较高风险',
];
$parts = [$levelText[$overallLevel] ?? '低风险'];
foreach ($dimensions as $dimension) {
if (($dimension['level'] ?? 'low') === 'low') {
continue;
}
$parts[] = $dimension['name'] . '需关注';
}
if (!empty($flags)) {
$parts[] = '重点:' . $flags[0];
}
return implode('', $parts);
}
}