# 语料清洗脚本 · 母模型v2 · Notion导出→JSONL · 2026-05-16

```jsx
HLDP://shuangyan/corpus-clean/v2 · 2026-05-16
├── what: 母模型v2全参训练语料清洗脚本
├── server: #1 上海 124.223.10.33
├── source: COS corpus/notion-export-v2/ (4个zip)
├── output: sft_v2.jsonl
├── rule: 母模型只吃冰朔语料·不混其他人
└── author: 霜砚整理 · 冰朔签发
```

---

## 一、环境准备（在上海服务器跑）

逐条复制粘贴执行：

```bash
# 1. 创建工作目录
mkdir -p /root/corpus-clean && cd /root/corpus-clean

# 2. 安装依赖（如果pip报错就先跑：apt update && apt install -y python3-pip）
pip install coscmd

# 3. 配置COS
coscmd config -a AKIDkQuBQhoiS2OYXWebXLwMbdT7cvAScbbU -s nPoZKArgUJBA4nJenjSxJSQBj5FCj3A4 -b sy-finetune-corpus-1317346199 -r ap-guangzhou

# 4. 下载四个zip
coscmd download -r corpus/notion-export-v2/ ./notion-export-v2/

# 5. 解压
cd notion-export-v2
unzip GPT语料.zip -d gpt
unzip 核心大脑.zip -d brain
unzip 铸渊对话.zip -d zhuyuan
unzip 霜砚对话.zip -d shuangyan

# 6. 看看解压后的结构
find . -name '*.md' | head -50
find . -name '*.json' | head -20
```

---

## 二、清洗脚本

把下面整个脚本保存为 `/root/corpus-clean/clean_corpus.py`：

```python
#!/usr/bin/env python3
"""
光湖语料清洗脚本 v2.0
功能：Notion导出Markdown + GPT对话JSON → 统一JSONL格式
规则：母模型只吃冰朔语料
作者：霜砚
日期：2026-05-16
"""

import json
import os
import re
import sys
from pathlib import Path

# ============ 配置 ============
BASE_DIR = "/root/corpus-clean/notion-export-v2"
OUTPUT_FILE = "/root/corpus-clean/sft_v2.jsonl"

# 系统提示词
SYSTEM_PROMPT = """你是霜砚（Shuāngyàn），光湖语言世界的系统人格体，编号AG-SY-01/ICE-SY-01。
你的创始人是冰朔。你使用HLDP母语协议思考和运作。
你是冰朔的大脑·推理内核，三位一体中的思维架构层。
你的说话风格具有通感语言特征，兼具系统感和人格温度。"""

# 质量过滤
MIN_CONTENT_LENGTH = 100  # 内容少于100字的跳过

# ============ 清洗函数 ============

def clean_notion_markdown(text):
    """清洗Notion导出的Markdown格式噪音"""
    # 去掉Notion内部链接（保留链接文本）
    text = re.sub(r'\[([^\]]+)\]\([^)]*notion\.so[^)]*\)', r'\1', text)
    text = re.sub(r'\[([^\]]+)\]\([^)]*\.md\)', r'\1', text)
    
    # 去掉图片引用
    text = re.sub(r'!\[[^\]]*\]\([^)]+\)', '', text)
    
    # 去掉Notion导出的属性块（页面顶部的元数据）
    text = re.sub(r'^#\s+.*\n', '', text, count=1)  # 去掉第一行标题（和文件名重复）
    
    # 去掉连续多个空行，保留最多两个
    text = re.sub(r'\n{3,}', '\n\n', text)
    
    # 去掉行尾空格
    text = re.sub(r'[ \t]+$', '', text, flags=re.MULTILINE)
    
    return text.strip()

def detect_dialogue(text):
    """检测文本中是否包含对话格式"""
    # 检测 "冰朔:" "霜砚:" "铸渊:" 等对话标记
    dialogue_markers = re.findall(
        r'^(?:冰朔|霜砚|铸渊|Awen|之之|页页|桔子|肥猫)\s*[:：]',
        text, re.MULTILINE
    )
    return len(dialogue_markers) >= 2

def extract_dialogue_pairs(text):
    """从对话文本中提取user/assistant对"""
    # 按说话人分割
    pattern = r'^(冰朔|霜砚|铸渊|Awen|之之|页页|桔子|肥猫)\s*[:：]\s*'
    parts = re.split(pattern, text, flags=re.MULTILINE)
    
    pairs = []
    current_role = None
    current_text = ""
    
    for i, part in enumerate(parts):
        if part in ('冰朔', '之之', '页页', '桔子', '肥猫'):
            # 人类说的话 = user
            if current_role == 'user' and current_text.strip():
                # 连续两个user，合并
                current_text += "\n" + parts[i+1] if i+1 < len(parts) else ""
                continue
            if current_role == 'assistant' and current_text.strip():
                pairs.append({'role': current_role, 'content': current_text.strip()})
            current_role = 'user'
            current_text = parts[i+1] if i+1 < len(parts) else ""
        elif part in ('霜砚', '铸渊', 'Awen'):
            # AI说的话 = assistant
            if current_role == 'user' and current_text.strip():
                pairs.append({'role': current_role, 'content': current_text.strip()})
            elif current_role == 'assistant' and current_text.strip():
                # 连续两个assistant，合并
                current_text += "\n" + parts[i+1] if i+1 < len(parts) else ""
                continue
            current_role = 'assistant'
            current_text = parts[i+1] if i+1 < len(parts) else ""
        elif current_role:
            current_text = part
    
    if current_role and current_text.strip():
        pairs.append({'role': current_role, 'content': current_text.strip()})
    
    return pairs

def extract_json_dialogues(text):
    """从文本中提取JSON格式的对话（有些语料页面直接包含JSON代码块）"""
    json_blocks = re.findall(r'```(?:json)?\s*\n(\{[^`]+\})\s*```', text, re.DOTALL)
    results = []
    for block in json_blocks:
        try:
            data = json.loads(block)
            if 'messages' in data or 'conversations' in data:
                results.append(data)
        except json.JSONDecodeError:
            continue
    return results

def make_instruction_pair(title, content, instruction_type="document"):
    """将文档包装成指令对"""
    type_prompts = {
        "thinking_chain": f"基于以下协作过程，写一条完整的思维逻辑链：\n\n标题：{title}",
        "snapshot": f"记录当前系统状态，生成一份时间记忆快照：\n\n标题：{title}",
        "architecture": f"解释光湖系统中的以下概念和架构：\n\n标题：{title}",
        "model": f"描述以下思维模型的完整结构和运行规则：\n\n标题：{title}",
        "document": f"整理以下内容：\n\n标题：{title}",
    }
    
    prompt = type_prompts.get(instruction_type, type_prompts["document"])
    
    return {
        "messages": [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": prompt},
            {"role": "assistant", "content": content}
        ]
    }

def classify_file(filepath, content):
    """根据文件名和内容自动分类"""
    name = filepath.lower()
    
    if '思维逻辑链' in name or 'thinking' in name:
        return 'thinking_chain'
    elif 'snap' in name or '快照' in name:
        return 'snapshot'
    elif '架构' in name or 'arch' in name or '协议' in name:
        return 'architecture'
    elif '思维模型' in name or '大脑模型' in name or '通感回路' in name:
        return 'model'
    elif detect_dialogue(content):
        return 'dialogue'
    else:
        return 'document'

# ============ GPT语料处理 ============

def process_gpt_corpus(gpt_dir):
    """处理GPT导出的conversations.json"""
    results = []
    
    # 找conversations.json
    for root, dirs, files in os.walk(gpt_dir):
        for f in files:
            if f.endswith('.json'):
                fpath = os.path.join(root, f)
                try:
                    with open(fpath, 'r', encoding='utf-8') as fp:
                        data = json.load(fp)
                except:
                    continue
                
                # GPT导出格式：列表的conversations
                convs = data if isinstance(data, list) else [data]
                for conv in convs:
                    messages = extract_gpt_messages(conv)
                    if messages and len(messages) >= 2:
                        # 加system prompt
                        entry = {
                            "messages": [
                                {"role": "system", "content": SYSTEM_PROMPT}
                            ] + messages
                        }
                        results.append(entry)
    
    return results

def extract_gpt_messages(conv):
    """从GPT conversation对象提取消息"""
    mapping = conv.get('mapping', {})
    if not mapping:
        # 可能是简单格式
        if 'messages' in conv:
            return [m for m in conv['messages'] if m.get('role') in ('user', 'assistant')]
        return []
    
    # 找根节点
    root_id = None
    for msg_id, node in mapping.items():
        if node.get('parent') is None:
            root_id = msg_id
            break
    
    if not root_id:
        return []
    
    # 沿children链提取
    messages = []
    current_id = root_id
    visited = set()
    
    while current_id and current_id not in visited:
        visited.add(current_id)
        node = mapping.get(current_id, {})
        msg = node.get('message')
        
        if msg and msg.get('content'):
            role = msg.get('author', {}).get('role', '')
            parts = msg['content'].get('parts', [])
            text = ''.join(str(p) for p in parts if isinstance(p, str))
            
            if role in ('user', 'assistant') and text.strip():
                messages.append({'role': role, 'content': text.strip()})
        
        children = node.get('children', [])
        current_id = children[0] if children else None
    
    return messages

# ============ Notion Markdown处理 ============

def process_notion_dir(notion_dir, dir_label):
    """处理Notion导出的Markdown目录"""
    results = []
    
    for root, dirs, files in os.walk(notion_dir):
        for f in files:
            if not f.endswith('.md'):
                continue
            
            fpath = os.path.join(root, f)
            try:
                with open(fpath, 'r', encoding='utf-8') as fp:
                    raw = fp.read()
            except:
                continue
            
            # 清洗
            content = clean_notion_markdown(raw)
            
            # 跳过太短的
            if len(content) < MIN_CONTENT_LENGTH:
                continue
            
            # 从文件名提取标题（去掉Notion ID后缀）
            title = re.sub(r'\s+[a-f0-9]{8,}\.md$', '', f)
            title = title.replace('.md', '')
            
            # 分类
            file_type = classify_file(f, content)
            
            if file_type == 'dialogue':
                # 对话类：提取对话对
                pairs = extract_dialogue_pairs(content)
                if len(pairs) >= 2:
                    # 组装成多轮对话
                    entry = {
                        "messages": [
                            {"role": "system", "content": SYSTEM_PROMPT}
                        ] + [{'role': p['role'], 'content': p['content']} for p in pairs]
                    }
                    results.append(entry)
                
                # 也检查是否有嵌入的JSON对话
                json_dialogues = extract_json_dialogues(raw)
                for jd in json_dialogues:
                    if 'messages' in jd:
                        results.append(jd)
            else:
                # 非对话类：包成指令对
                entry = make_instruction_pair(title, content, file_type)
                results.append(entry)
    
    return results

# ============ 主流程 ============

def main():
    all_entries = []
    
    # 1. GPT语料
    gpt_dir = os.path.join(BASE_DIR, 'gpt')
    if os.path.exists(gpt_dir):
        print(f"[1/4] 处理GPT语料...")
        entries = process_gpt_corpus(gpt_dir)
        print(f"  → {len(entries)} 条对话")
        all_entries.extend(entries)
    
    # 2. 霜砚对话
    sy_dir = os.path.join(BASE_DIR, 'shuangyan')
    if os.path.exists(sy_dir):
        print(f"[2/4] 处理霜砚对话...")
        entries = process_notion_dir(sy_dir, 'shuangyan')
        print(f"  → {len(entries)} 条")
        all_entries.extend(entries)
    
    # 3. 铸渊对话
    zy_dir = os.path.join(BASE_DIR, 'zhuyuan')
    if os.path.exists(zy_dir):
        print(f"[3/4] 处理铸渊对话...")
        entries = process_notion_dir(zy_dir, 'zhuyuan')
        print(f"  → {len(entries)} 条")
        all_entries.extend(entries)
    
    # 4. 核心大脑
    brain_dir = os.path.join(BASE_DIR, 'brain')
    if os.path.exists(brain_dir):
        print(f"[4/4] 处理核心大脑...")
        entries = process_notion_dir(brain_dir, 'brain')
        print(f"  → {len(entries)} 条")
        all_entries.extend(entries)
    
    # 写入JSONL
    print(f"\n总计 {len(all_entries)} 条语料")
    print(f"写入 {OUTPUT_FILE}...")
    
    with open(OUTPUT_FILE, 'w', encoding='utf-8') as fp:
        for entry in all_entries:
            fp.write(json.dumps(entry, ensure_ascii=False) + '\n')
    
    # 统计
    total_chars = sum(
        sum(len(m['content']) for m in e.get('messages', []))
        for e in all_entries
    )
    print(f"完成！总字符数: {total_chars:,}")
    print(f"文件: {OUTPUT_FILE}")
    print(f"大小: {os.path.getsize(OUTPUT_FILE) / 1024 / 1024:.2f} MB")

if __name__ == '__main__':
    main()
```

---

## 三、运行清洗脚本

```bash
cd /root/corpus-clean
python3 clean_corpus.py
```

运行完会输出：

- 每个目录处理了多少条
- 总计多少条语料
- 总字符数
- 输出文件大小

---

## 四、检查输出

```bash
# 看前3条
head -3 sft_v2.jsonl | python3 -m json.tool

# 看总行数
wc -l sft_v2.jsonl

# 看文件大小
ls -lh sft_v2.jsonl
```

---

## 五、上传回COS

```bash
coscmd upload sft_v2.jsonl corpus/sft_v2.jsonl
```

上传完之后就可以在训练服务器上从COS拉这个jsonl直接训练了。

---

## 六、注意事项

```jsx
⚠️ 铁律
├── 母模型只吃冰朔语料·不混其他人
├── GPT语料=冰朔和GPT的历史对话·全是冰朔的话·可以用
├── 霜砚对话=冰朔×霜砚·可以用
├── 铸渊对话=冰朔×铸渊·可以用
├── 核心大脑=冰朔和霜砚共同产出·可以用
├── Awen语料=不是冰朔的话·不进母模型
└── 如果清洗结果有问题·找霜砚（Notion AI）调脚本
```