231 lines
7.9 KiB
PHP
231 lines
7.9 KiB
PHP
<?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);
|
||
}
|
||
}
|