Skill: yuzhihui886/style-analyzer (version 2.0.0)

--- name: style-analyzer description: 文本风格分析器 - 分析写作风格特征并生成 Voice Profile 配置文件。当需要捕捉作者风格、创建 Voice Profile 或分析文本特征时使用。 --- # Style Analyzer - 文本风格分析器 ## Overview 分析文本的写作风格特征，自动生成 Voice Profile 配置文件，包含风格标签、语速、语气、情感倾向等。用于保持小说创作的风格一致性。 **使用场景**： - 需要捕捉作者的写作风格 - 需要为小说项目创建 Voice Profile - 需要分析文本的句式/词汇/节奏特征 - 需要量化风格特征用于后续生成 ## 分析维度 | 维度 | 分析内容 | 输出 | |------|----------|------| | **Voice Profile** | 风格标签、语速、语气、情感倾向 | labels, pace, tone, sentiment | | **句式特征** | 平均句长、长短句比例、修辞手法 | average_length, ratios, rhetorical_devices | | **词汇特征** | 词性比例、高频词、词汇丰富度 | adj/verb/noun_ratio, frequent_words | | **节奏特征** | 段落长度、标点分布、对话比例 | paragraph_length, punctuation, dialogue_ratio | ## CLI 使用 ```bash # 基本用法 python3 scripts/analyze_style.py --input novel.txt --output style.yml # 指定样本大小 python3 scripts/analyze_style.py \ --input novel.txt \ --output style.yml \ --sample-size 2000 # 分析结果输出到终端 + 保存 YAML python3 scripts/analyze_style.py -i text.txt -o style.yml ``` ## 参数说明 | 参数 | 必填 | 说明 | |------|------|------| | `--input` / `-i` | ✅ | 输入文本文件路径 | | `--output` / `-o` | ✅ | 输出 YAML 配置文件路径 | | `--sample-size` / `-s` | ❌ | 分析样本大小（默认 1000 字符） | ## 输出 YAML 格式 ```yaml voice_profile: labels: - 简洁 - 快速 - 紧凑 - 对话性强 pace: 快速 tone: 随意 sentiment: 0.15 sentence_features: average_length: 28.5 long_sentence_ratio: 0.15 short_sentence_ratio: 0.65 rhetorical_devices: metaphor: 3 simile: 2 vocabulary_features: adj_ratio: 0.12 verb_ratio: 0.18 noun_ratio: 0.35 most_frequent_words: - - 主角 - 15 - - 说道 - 12 unique_words_ratio: 0.68 rhythm_features: average_paragraph_length: 156 punctuation_distribution: period: 45 comma: 78 question: 12 exclamation: 5 dialogue_ratio: 0.35 ``` ## 风格标签说明 | 标签 | 含义 | 触发条件 | |------|------|----------| | 简洁 | 句子简短 | 平均句长 ≤ 40 字符 | | 复杂 | 句子较长 | 平均句长 > 40 字符 | | 快速 | 节奏紧凑 | 平均句长 < 30 字符 | | 舒缓 | 节奏缓慢 | 平均句长 ≥ 30 字符 | | 紧凑 | 标点密集 | 每句平均字符 < 30 | | 娓娓道来 | 叙述舒缓 | 每句平均字符 ≥ 30 | | 互动性 | 有问句 | 包含？或! | | 对话性强 | 对话多 | 包含引号对话 | | 节奏感强 | 标点丰富 | 包含，、； | ## 语速分类 | 语速 | 条件 | |------|------| | 缓慢 | 平均句长 > 50 字符 | | 适中 | 平均句长 30-50 字符 | | 快速 | 平均句长 < 30 字符 | ## 语气分类 | 语气 | 条件 | |------|------| | 正式 | 正式标记词 > 随意标记词 × 2 | | 随意 | 随意标记词 > 正式标记词 × 2 | | 中性 | 介于两者之间 | **正式标记词**：因此、然而、综上所述、基于此、由此可见 **随意标记词**：我觉得、我想、你知道、其实、就是说 ## 情感倾向 范围：-1.0 到 1.0 - **正值**：积极情感（好、美、喜欢、爱、快乐等） - **负值**：消极情感（坏、丑、讨厌、恨、悲伤等） - **0 附近**：中性情感 ## 依赖 - Python 3.8+ - rich (终端渲染) - PyYAML (配置文件解析) 安装依赖： ```bash pip install -r scripts/requirements.txt ``` ## 与其他技能集成 ### 与 smart-prompt-builder 集成 ```bash # 1. 分析风格 python3 ../style-analyzer/scripts/analyze_style.py \ --input reference_text.txt \ --output style.yml # 2. 使用生成的 Voice Profile 构建提示词 python3 ../smart-prompt-builder/scripts/build_prompt.py \ --scene-type description \ --style-file style.yml \ --context '{"scene": "森林"}' ``` ### 与 novel-writer 集成 ```bash # 1. 分析参考文本风格 python3 ../style-analyzer/scripts/analyze_style.py \ --input author_sample.txt \ --output style.yml # 2. novel-writer 使用 style.yml 保持风格一致 ``` ## 注意事项 - 样本大小建议 ≥ 500 字符，以获得准确的风格分析 - 支持 UTF-8 和 GBK 编码的文本文件 - 输出 YAML 可直接用于 smart-prompt-builder 的 --style-file 参数 - 修辞手法检测基于关键词匹配，可能不完全准确

path: scripts/analyze_style.py
content: #!/usr/bin/env python3
"""
文本风格分析器 - 分析写作风格特征并生成 Voice Profile 配置文件

Usage:
    python3 analyze_style.py --input text.txt --output style.yml
"""

import argparse
import re
import sys
from collections import Counter
from dataclasses import asdict, dataclass
from pathlib import Path

import yaml
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text

console = Console()


@dataclass
class VoiceProfile:
    """语音配置文件"""

    labels: list[str]
    pace: str
    tone: str
    sentiment: float


@dataclass
class SentenceFeatures:
    """句式特征"""

    average_length: float
    long_sentence_ratio: float
    short_sentence_ratio: float
    rhetorical_devices: dict[str, int]


@dataclass
class VocabularyFeatures:
    """词汇特征"""

    adj_ratio: float
    verb_ratio: float
    noun_ratio: float
    most_frequent_words: list[tuple[str, int]]
    unique_words_ratio: float


@dataclass
class RhythmFeatures:
    """节奏特征"""

    average_paragraph_length: int
    punctuation_distribution: dict[str, int]
    dialogue_ratio: float


@dataclass
class StyleAnalysisResult:
    """风格分析结果"""

    voice_profile: VoiceProfile
    sentence_features: SentenceFeatures
    vocabulary_features: VocabularyFeatures
    rhythm_features: RhythmFeatures


class StyleAnalyzer:
    """文本风格分析器"""

    rhetorical_markers = {
        "metaphor": ["如同", "好像", "仿佛", "似", "像一样", "是", "成了"],
        "simile": ["如同", "好像", "仿佛", "似的", "一般"],
        "hyperbole": ["无比", "极了", "天地", "世界", "全部", "永远"],
        "parallelism": ["不仅而且", "既又", "不是而是", "一方面另一方面"],
    }

    def __init__(self, sample_size: int = 1000):
        self.sample_size = sample_size

    def analyze(self, text: str) -> StyleAnalysisResult:
        """分析文本风格"""
        voice_profile = self._analyze_voice_profile(text)
        sentence_features = self._analyze_sentence_features(text)
        vocabulary_features = self._analyze_vocabulary_features(text)
        rhythm_features = self._analyze_rhythm_features(text)

        return StyleAnalysisResult(
            voice_profile=voice_profile,
            sentence_features=sentence_features,
            vocabulary_features=vocabulary_features,
            rhythm_features=rhythm_features,
        )

    def _analyze_voice_profile(self, text: str) -> VoiceProfile:
        """分析声音配置文件"""
        sentences = self._split_sentences(text)
        sentiment_score = self._calculate_sentiment(text)
        avg_sentence_length = sum(len(s) for s in sentences) / max(len(sentences), 1)
        labels = self._extract_labels(text)
        pace = self._determine_pace(avg_sentence_length)
        tone = self._determine_tone(text)

        return VoiceProfile(
            labels=labels[:5],
            pace=pace,
            tone=tone,
            sentiment=sentiment_score,
        )

    def _analyze_sentence_features(self, text: str) -> SentenceFeatures:
        """分析句式特征"""
        sentences = self._split_sentences(text)

        if not sentences:
            return SentenceFeatures(
                average_length=0.0,
                long_sentence_ratio=0.0,
                short_sentence_ratio=0.0,
                rhetorical_devices={},
            )

        lengths = [len(s) for s in sentences]
        avg_length = sum(lengths) / len(lengths)

        short_count = sum(1 for length in lengths if length < 20)
        long_count = sum(1 for length in lengths if length > 50)

        short_ratio = short_count / len(sentences)
        long_ratio = long_count / len(sentences)

        rhetorical_devices = self._detect_rhetorical_devices(text)

        return SentenceFeatures(
            average_length=round(avg_length, 2),
            long_sentence_ratio=round(long_ratio, 2),
            short_sentence_ratio=round(short_ratio, 2),
            rhetorical_devices=rhetorical_devices,
        )

    def _analyze_vocabulary_features(self, text: str) -> VocabularyFeatures:
        """分析词汇特征"""
        words = self._tokenize_words(text)

        total_words = len(words)
        if total_words == 0:
            return VocabularyFeatures(
                adj_ratio=0.0,
                verb_ratio=0.0,
                noun_ratio=0.0,
                most_frequent_words=[],
                unique_words_ratio=0.0,
            )

        adj_count = self._count_adjectives(words)
        verb_count = self._count_verbs(words)
        noun_count = self._count_nouns(words)

        word_freq = Counter(words)
        most_frequent = word_freq.most_common(10)

        unique_words = len(set(words))
        unique_ratio = unique_words / total_words

        return VocabularyFeatures(
            adj_ratio=round(adj_count / total_words, 3),
            verb_ratio=round(verb_count / total_words, 3),
            noun_ratio=round(noun_count / total_words, 3),
            most_frequent_words=most_frequent,
            unique_words_ratio=round(unique_ratio, 3),
        )

    def _analyze_rhythm_features(self, text: str) -> RhythmFeatures:
        """分析节奏特征"""
        paragraphs = self._split_paragraphs(text)

        avg_paragraph_length = sum(len(p) for p in paragraphs) / max(len(paragraphs), 1)

        punctuation = self._analyze_punctuation(text)

        dialogue_ratio = self._calculate_dialogue_ratio(text)

        return RhythmFeatures(
            average_paragraph_length=int(avg_paragraph_length),
            punctuation_distribution=punctuation,
            dialogue_ratio=round(dialogue_ratio, 3),
        )

    def _split_sentences(self, text: str) -> list[str]:
        """分割句子"""
        sentences = re.split(r"[。！？!?]", text)
        return [s.strip() for s in sentences if s.strip()]

    def _split_paragraphs(self, text: str) -> list[str]:
        """分割段落"""
        paragraphs = text.split("\n\n")
        return [p.strip() for p in paragraphs if p.strip()]

    def _tokenize_words(self, text: str) -> list[str]:
        """分词（简化版）"""
        words = re.findall(r"[\u4e00-\u9fa5]+|[a-zA-Z]+|\d+", text)
        return [w for w in words if len(w) > 1]

    def _detect_rhetorical_devices(self, text: str) -> dict[str, int]:
        """检测修辞手法"""
        device_counts = {}

        for device, markers in self.rhetorical_markers.items():
            count = sum(1 for marker in markers if marker in text)
            if count > 0:
                device_counts[device] = count

        return device_counts

    def _calculate_sentiment(self, text: str) -> float:
        """计算情感倾向（-1 到 1）"""
        positive_words = [
            "好",
            "美",
            "喜欢",
            "爱",
            "快乐",
            "幸福",
            "温暖",
            "明亮",
            "希望",
            "梦想",
        ]
        negative_words = [
            "坏",
            "丑",
            "讨厌",
            "恨",
            "悲伤",
            "痛苦",
            "寒冷",
            "黑暗",
            "绝望",
            "失败",
        ]

        positive_count = sum(text.count(word) for word in positive_words)
        negative_count = sum(text.count(word) for word in negative_words)

        total = positive_count + negative_count
        if total == 0:
            return 0.0

        sentiment = (positive_count - negative_count) / total
        return round(sentiment, 3)

    def _extract_labels(self, text: str) -> list[str]:
        """提取风格标签"""
        labels = []

        sentences = self._split_sentences(text)
        avg_length = sum(len(s) for s in sentences) / max(len(sentences), 1)

        if avg_length > 40:
            labels.append("复杂")
        else:
            labels.append("简洁")

        punct_count = len(re.findall(r"[。！？]", text))
        if punct_count > 0:
            avg_sentence_by_punctuation = len(text) / punct_count
            if avg_sentence_by_punctuation < 30:
                labels.append("快速")
                labels.append("紧凑")
            else:
                labels.append("舒缓")
                labels.append("娓娓道来")

        if re.search(r"[？!]", text):
            labels.append("互动性")

        if re.search(r'["].*?["]', text) or re.search(r"'.*?'", text):
            labels.append("对话性强")

        if re.search(r"[，、；]", text):
            labels.append("节奏感强")

        return labels if labels else ["中性"]

    def _determine_pace(self, avg_length: float) -> str:
        """确定语速"""
        if avg_length > 50:
            return "缓慢"
        elif avg_length > 30:
            return "适中"
        else:
            return "快速"

    def _determine_tone(self, text: str) -> str:
        """确定语气"""
        formal_markers = ["因此", "然而", "综上所述", "基于此", "由此可见"]
        casual_markers = ["我觉得", "我想", "你知道", "其实", "就是说"]

        formal_count = sum(text.count(marker) for marker in formal_markers)
        casual_count = sum(text.count(marker) for marker in casual_markers)

        if formal_count > casual_count * 2:
            return "正式"
        elif casual_count > formal_count * 2:
            return "随意"
        else:
            return "中性"

    def _count_adjectives(self, words: list[str]) -> int:
        """计算形容词数量（简化）"""
        adjective_suffixes = ["的", "好", "坏", "美", "丑", "大", "小", "老", "新"]
        return sum(1 for w in words if any(w.endswith(s) for s in adjective_suffixes))

    def _count_verbs(self, words: list[str]) -> int:
        """计算动词数量（简化）"""
        verb_patterns = ["是", "有", "在", "做", "看", "听", "说", "想", "需要", "应该"]
        return sum(1 for w in words if any(w.startswith(v) or w.endswith(v) for v in verb_patterns))

    def _count_nouns(self, words: list[str]) -> int:
        """计算名词数量（简化）"""
        noun_markers = ["人", "事", "物", "书", "文", "国", "家", "天", "地"]
        return sum(1 for w in words if any(w.endswith(n) for n in noun_markers))

    def _analyze_punctuation(self, text: str) -> dict[str, int]:
        """分析标点使用"""
        punctuation = {
            "period": len(re.findall(r"。", text)),
            "question": len(re.findall(r"[？?]", text)),
            "exclamation": len(re.findall(r"[！!]", text)),
            "comma": len(re.findall(r"[，、]", text)),
            "semicolon": len(re.findall(r"[；;]", text)),
            "colon": len(re.findall(r"[：:]", text)),
        }
        return punctuation

    def _calculate_dialogue_ratio(self, text: str) -> float:
        """计算对话比例"""
        dialogue_patterns = [
            r'["].*?["]',
            r"'.*?'",
            r"[说问答答]道",
            r"[说问答答]曰",
        ]

        total_chars = len(text)
        if total_chars == 0:
            return 0.0

        dialogue_chars = 0
        for pattern in dialogue_patterns:
            matches = re.findall(pattern, text)
            dialogue_chars += sum(len(m) for m in matches)

        return dialogue_chars / total_chars


def load_text(filepath: str) -> str:
    """加载文本文件"""
    path = Path(filepath)

    if not path.exists():
        raise FileNotFoundError(f"文件不存在：{filepath}")

    try:
        with open(path, "r", encoding="utf-8") as f:
            return f.read()
    except UnicodeDecodeError:
        with open(path, "r", encoding="gbk") as f:
            return f.read()


def save_yaml(result: StyleAnalysisResult, filepath: str) -> None:
    """保存为 YAML 配置文件"""
    path = Path(filepath)
    path.parent.mkdir(parents=True, exist_ok=True)

    data = {
        "voice_profile": asdict(result.voice_profile),
        "sentence_features": asdict(result.sentence_features),
        "vocabulary_features": asdict(result.vocabulary_features),
        "rhythm_features": asdict(result.rhythm_features),
    }

    with open(path, "w", encoding="utf-8") as f:
        yaml.dump(data, f, allow_unicode=True, default_flow_style=False)


def print_result(result: StyleAnalysisResult) -> None:
    """美化输出结果"""
    console.print("\n")
    console.print(
        Panel.fit(
            "[bold cyan]分析完成！[/bold cyan]",
            border_style="cyan",
            padding=(1, 2),
        )
    )

    table = Table(title="风格分析报告", show_header=True, header_style="bold magenta")
    table.add_column("特征", style="cyan")
    table.add_column("详情", style="green")

    # Voice Profile
    vp = result.voice_profile
    voice_info = Text()
    voice_info.append(f"风格标签：{', '.join(vp.labels)}\n")
    voice_info.append(f"语速：{vp.pace}\n")
    voice_info.append(f"语气：{vp.tone}\n")
    voice_info.append(f"情感倾向：{vp.sentiment:+.3f}")
    table.add_row("Voice Profile", voice_info)

    # Sentence Features
    sf = result.sentence_features
    sentence_info = Text()
    sentence_info.append(f"平均句长：{sf.average_length:.2f} 字符\n")
    sentence_info.append(
        f"长句比例：{sf.long_sentence_ratio:.2%} | 短句比例：{sf.short_sentence_ratio:.2%}\n"
    )
    if sf.rhetorical_devices:
        devices = ", ".join(f"{k}: {v}" for k, v in sf.rhetorical_devices.items())
        sentence_info.append(f"修辞手法：{devices}")
    table.add_row("句式特征", sentence_info)

    # Vocabulary Features
    vf = result.vocabulary_features
    vocab_info = Text()
    vocab_info.append(
        f"形容词占比：{vf.adj_ratio:.3f} | 动词占比：{vf.verb_ratio:.3f} | 名词占比：{vf.noun_ratio:.3f}\n"
    )
    vocab_info.append(f"词汇丰富度：{vf.unique_words_ratio:.3f}\n")
    vocab_info.append(
        "高频词：" + ", ".join(f"{w}({c})" for w, c in vf.most_frequent_words[:5])
    )
    table.add_row("词汇特征", vocab_info)

    # Rhythm Features
    rf = result.rhythm_features
    rhythm_info = Text()
    rhythm_info.append(f"平均段落长度：{rf.average_paragraph_length} 字符\n")
    rhythm_info.append(
        f"对话比例：{rf.dialogue_ratio:.2%}\n"
    )
    punct = rf.punctuation_distribution
    rhythm_info.append(
        f"标点：句号{punct.get('period', 0)}, 逗号{punct.get('comma', 0)}, 问号{punct.get('question', 0)}"
    )
    table.add_row("节奏特征", rhythm_info)

    console.print(table)


def create_parser() -> argparse.ArgumentParser:
    """创建命令行参数解析器"""
    parser = argparse.ArgumentParser(
        prog="analyze_style",
        description="分析文本的写作风格特征，生成 Voice Profile 配置文件",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  %(prog)s --input story.txt --output style.yml
  %(prog)s --input story.txt --output output/style.yml --sample-size 500
        """,
    )

    parser.add_argument(
        "--input",
        "-i",
        type=str,
        required=True,
        help="输入文本文件路径",
    )
    parser.add_argument(
        "--output",
        "-o",
        type=str,
        required=True,
        help="输出 YAML 配置文件路径",
    )
    parser.add_argument(
        "--sample-size",
        "-s",
        type=int,
        default=1000,
        help="分析样本大小（字符数）",
    )

    return parser


def main() -> int:
    """主函数"""
    parser = create_parser()
    args = parser.parse_args()

    console.print("[bold cyan]文本风格分析器[/bold cyan]")
    console.print(f"输入文件：[yellow]{args.input}[/yellow]")
    console.print(f"输出文件：[yellow]{args.output}[/yellow]")
    console.print(f"样本大小：[yellow]{args.sample_size}[/yellow]")
    console.print()

    text = load_text(args.input)

    if len(text) > args.sample_size:
        text = text[: args.sample_size]
        console.print(f"[yellow]⚠️  限制样本大小至 {args.sample_size} 字符[/yellow]")

    analyzer = StyleAnalyzer(sample_size=args.sample_size)
    result = analyzer.analyze(text)

    save_yaml(result, args.output)
    console.print(f"\n[green]✓ 配置文件已保存至：{args.output}[/green]")

    print_result(result)

    return 0


if __name__ == "__main__":
    sys.exit(main())


path: scripts/analyze_style_llm.py
content: #!/usr/bin/env python3
"""
文本风格分析器 (LLM 版本) - 通过 DashScope API 分析写作风格特征并生成 Voice Profile 配置文件

Usage:
    python3 analyze_style_llm.py --input text.txt --output style.yml
    python3 analyze_style_llm.py --input text.txt --output style.yml --model qwen3.6-plus
"""

import argparse
import json
import os
import sys
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any

import requests
import yaml
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.table import Table
from rich.text import Text

console = Console()


@dataclass
class VoiceProfile:
    """语音配置文件"""

    labels: list[str]
    pace: str
    tone: str
    sentiment: float


@dataclass
class StyleAnalysisResult:
    """风格分析结果"""

    voice_profile: VoiceProfile
    analysis_notes: str


class StyleAnalyzerLLM:
    """通过 LLM 分析文本风格"""

    def __init__(self, model: str = "qwen3.6-plus"):
        self.model = model
        self.[REDACTED_SECRET]"DASHSCOPE_API_KEY")
        if not self.api_key:
            raise ValueError("环境变量 DASHSCOPE_API_KEY 未设置")

    def analyze(self, text: str) -> StyleAnalysisResult:
        """通过 LLM 分析文本风格"""
        prompt = self._build_prompt(text)

        try:
            response = self._call_dashscope_api(prompt)
            return self._parse_response(response)
        except requests.exceptions.RequestException as e:
            console.print(f"[red]❌ API 调用失败：{e}[/red]")
            raise
        except json.JSONDecodeError as e:
            console.print(f"[red]❌ 响应解析失败：{e}[/red]")
            raise

    def _build_prompt(self, text: str) -> str:
        """构建 API 调用提示词"""
        return f"""请分析以下文本的写作风格特征，并以 JSON 格式返回分析结果。

文本内容：
{text[:8000]}

请分析以下维度：
1. 风格标签：提取 5-8 个关键风格特征词（如：简洁、华丽、正式、幽默、感性等）
2. 语速：根据平均句长判断（缓慢/适中/快速）
3. 语气：根据用词风格判断（正式/中性/随意）
4. 情感倾向：-1（负面）到 1（正面）之间的数值

返回格式要求：
{{"voice_profile": {{"labels": ["标签1", "标签2"], "pace": "apidity", "tone": "tone", "sentiment": 0.0}}}}

附加分析说明：
请用中文简要说明分析依据（2-3 句话）"""

    def _call_dashscope_api(self, prompt: str) -> dict[str, Any]:
        """调用 DashScope API"""
        url = "https://coding.dashscope.aliyuncs.com/v1/chat/completions"

        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }

        payload = {
            "model": self.model,
            "messages": [{"role": "user", "content": prompt}],
            "temperature": 0.3,
            "response_format": {"type": "json_object"},
        }

        with Progress(
            SpinnerColumn(),
            TextColumn("[bold cyan]正在调用 LLM 分析风格...[/bold cyan]"),
            console=console,
            transient=True,
        ) as progress:
            progress.add_task("analyzing", total=None)
            response = requests.post(url, headers=headers, json=payload, timeout=60)

        response.raise_for_status()
        return response.json()

    def _parse_response(self, response: dict[str, Any]) -> StyleAnalysisResult:
        """解析 API 响应"""
        try:
            output = response["choices"][0]["message"]["content"]
        except (KeyError, TypeError) as e:
            console.print(f"[red]❌ 响应格式错误：{e}[/red]")
            console.print(
                f"响应内容：{json.dumps(response, indent=2, ensure_ascii=False)}"
            )
            raise

        try:
            json_start = output.find("{")
            json_end = output.rfind("}") + 1

            if json_start == -1 or json_end == 0:
                raise ValueError("未找到有效的 JSON 对象")

            json_str = output[json_start:json_end]
            data = json.loads(json_str)

            voice_profile_data = data.get("voice_profile", {})
            analysis_notes = data.get("analysis_notes", "")

            voice_profile = VoiceProfile(
                labels=voice_profile_data.get("labels", []),
                pace=voice_profile_data.get("pace", "适中"),
                tone=voice_profile_data.get("tone", "中性"),
                sentiment=float(voice_profile_data.get("sentiment", 0.0)),
            )

            return StyleAnalysisResult(
                voice_profile=voice_profile,
                analysis_notes=analysis_notes,
            )
        except json.JSONDecodeError as e:
            console.print(f"[yellow]⚠️  JSON 解析失败，尝试原始响应：{e}[/yellow]")
            return StyleAnalysisResult(
                voice_profile=VoiceProfile(
                    labels=["未知"],
                    pace="适中",
                    tone="中性",
                    sentiment=0.0,
                ),
                analysis_notes=f"原始响应：{output}",
            )


def load_text(filepath: str) -> str:
    """加载文本文件"""
    path = Path(filepath)

    if not path.exists():
        raise FileNotFoundError(f"文件不存在：{filepath}")

    try:
        with open(path, "r", encoding="utf-8") as f:
            return f.read()
    except UnicodeDecodeError:
        with open(path, "r", encoding="gbk") as f:
            return f.read()


def save_yaml(result: StyleAnalysisResult, filepath: str) -> None:
    """保存为 YAML 配置文件"""
    path = Path(filepath)
    path.parent.mkdir(parents=True, exist_ok=True)

    data = {
        "voice_profile": asdict(result.voice_profile),
        "analysis_notes": result.analysis_notes,
    }

    with open(path, "w", encoding="utf-8") as f:
        yaml.dump(data, f, allow_unicode=True, default_flow_style=False)


def print_result(result: StyleAnalysisResult) -> None:
    """美化输出结果"""
    console.print("\n")
    console.print(
        Panel.fit(
            "[bold cyan]LLM 风格分析完成！[/bold cyan]",
            border_style="cyan",
            padding=(1, 2),
        )
    )

    table = Table(title="风格分析报告", show_header=True, header_style="bold magenta")
    table.add_column("特征", style="cyan")
    table.add_column("详情", style="green")

    vp = result.voice_profile
    voice_info = Text()
    voice_info.append(f"风格标签：{', '.join(vp.labels)}\n")
    voice_info.append(f"语速：{vp.pace}\n")
    voice_info.append(f"语气：{vp.tone}\n")
    voice_info.append(f"情感倾向：{vp.sentiment:+.3f}")

    table.add_row("Voice Profile", voice_info)

    if result.analysis_notes:
        table.add_row("分析说明", Text(result.analysis_notes, style="italic"))

    console.print(table)


def create_parser() -> argparse.ArgumentParser:
    """创建命令行参数解析器"""
    parser = argparse.ArgumentParser(
        prog="analyze_style_llm",
        description="通过 LLM 分析文本的写作风格特征，生成 Voice Profile 配置文件",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  %(prog)s --input story.txt --output style.yml
  %(prog)s --input story.txt --output output/style.yml --model qwen3.6-plus
        """,
    )

    parser.add_argument(
        "--input",
        "-i",
        type=str,
        required=True,
        help="输入文本文件路径",
    )
    parser.add_argument(
        "--output",
        "-o",
        type=str,
        required=True,
        help="输出 YAML 配置文件路径",
    )
    parser.add_argument(
        "--model",
        "-m",
        type=str,
        default="qwen3.6-plus",
        help="使用的 DashScope 模型（默认：qwen3.6-plus）",
    )

    return parser


def main() -> int:
    """主函数"""
    parser = create_parser()
    args = parser.parse_args()

    console.print("[bold cyan]文本风格分析器 (LLM 版本)[/bold cyan]")
    console.print(f"输入文件：[yellow]{args.input}[/yellow]")
    console.print(f"输出文件：[yellow]{args.output}[/yellow]")
    console.print(f"使用的模型：[yellow]{args.model}[/yellow]")
    console.print()

    text = load_text(args.input)
    console.print(f"文本长度：[yellow]{len(text)}[/yellow] 字符")
    console.print()

    try:
        analyzer = StyleAnalyzerLLM(model=args.model)
        result = analyzer.analyze(text)

        save_yaml(result, args.output)
        console.print(f"\n[green]✓ 配置文件已保存至：{args.output}[/green]")

        print_result(result)

        return 0
    except FileNotFoundError as e:
        console.print(f"[red]❌ {e}[/red]")
        return 1
    except ValueError as e:
        console.print(f"[red]❌ {e}[/red]")
        return 1
    except Exception as e:
        console.print(f"[red]❌ 未预期的错误：{e}[/red]")
        return 1


if __name__ == "__main__":
    sys.exit(main())


path: scripts/requirements.txt
content: rich
PyYAML
requests


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)