File size: 13,082 Bytes
993e6a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import sys
import re
import json
import requests
import threading
from concurrent.futures import ThreadPoolExecutor
from typing import List, Dict, Tuple

sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import api_key, base_url

# API configuration
API_KEY = api_key
API_PROVIDER = base_url

# Thread-safe print function
print_lock = threading.Lock()
def thread_safe_print(*args, **kwargs):
    with print_lock:
        print(*args, **kwargs)

def query_llm(prompt: str) -> str:
    """
    Query LLM API with a prompt
    Args:
        prompt: The prompt to send to LLM
    Returns:
        str: The response from LLM
    """
    headers = {
        'Authorization': f'Bearer {API_KEY}',
        'Content-Type': 'application/json'
    }
    
    data = {
        'model': 'gpt-5-mini',
        'messages': [
            {
                'role': 'system', 
                'content': 'You are a senior data journalist and infographic designer specialized in creating compelling data stories. Always return valid JSON format only, without any markdown formatting or extra text.'
            },
            {
                'role': 'user', 
                'content': prompt
            }
        ],
        'temperature': 0.7
    }
    
    try:
        response = requests.post(
            f'{API_PROVIDER}/chat/completions',
            headers=headers,
            json=data,
            timeout=120
        )
        response.raise_for_status()
        
        result = response.json()
        return result['choices'][0]['message']['content'].strip()
        
    except requests.exceptions.Timeout:
        thread_safe_print("❌ LLM API 超时(60秒)")
        return None
    except requests.exceptions.HTTPError as e:
        thread_safe_print(f"❌ LLM API HTTP 错误: {e}")
        if hasattr(e.response, 'text'):
            thread_safe_print(f"   响应: {e.response.text[:200]}")
        return None
    except requests.exceptions.RequestException as e:
        thread_safe_print(f"❌ LLM API 请求错误: {e}")
        return None
    except KeyError as e:
        thread_safe_print(f"❌ LLM API 响应格式错误: {e}")
        return None
    except Exception as e:
        thread_safe_print(f"❌ 查询 LLM 时出错: {e}")
        return None

def read_theme_file(file_path: str) -> Dict[str, List[Dict[str, str]]]:
    """
    Read the theme file and parse it into a dictionary with detailed themes
    Args:
        file_path: Path to the theme file
    Returns:
        Dict[str, List[Dict]]: Dictionary where keys are main theme names and values are lists of 
                              dictionaries containing specific themes with their number and text
    """
    themes = {}
    current_main_theme = None
    
    with open(file_path, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
                
            if line.startswith('#'):
                current_main_theme = line[1:].strip()
                themes[current_main_theme] = []
            elif current_main_theme and re.match(r'^\d+\.', line):
                # Extract the number and the specific theme content
                match = re.match(r'^(\d+)\.\s*(.*)', line)
                if match:
                    number = int(match.group(1))
                    specific_theme = match.group(2)
                    themes[current_main_theme].append({
                        "number": number,
                        "theme": specific_theme
                    })
    
    return themes

def generate_similar_themes(main_theme: str, specific_theme: str, count: int = 15) -> List[Dict]:
    """
    Generate similar themes to a specific theme and return in JSON format
    Args:
        main_theme: The main theme category
        specific_theme: The specific theme to generate similar themes for
        count: Number of similar themes to generate
    Returns:
        List[Dict]: List of new theme dictionaries with id, theme, and description
    """
    prompt = f"""
You are a senior data journalist and infographic designer. Generate {count} compelling, diverse data story themes inspired by this reference:

Main Category: {main_theme}
Reference Theme: {specific_theme}

Create themes that would make excellent real-world infographics with these characteristics:

**DIVERSITY REQUIREMENTS:**
- Mix different angles: comparisons, trends over time, geographic distributions, rankings, cause-effect relationships, surprising statistics, myth-busting facts
- Vary the scope: global, regional, national, city-level, industry-specific, demographic-specific
- Include different time frames: historical analysis, current snapshots, future projections
- Cover various data types: percentages, absolute numbers, ratios, growth rates, correlations

**QUALITY CRITERIA:**
1. Each theme should tell a compelling data story that surprises, educates, or reveals hidden patterns
2. Must be based on realistic, obtainable data (surveys, government statistics, research studies, industry reports)
3. Should have a clear "hook" - why would someone stop scrolling to look at this infographic?
4. Include specific, concrete angles (e.g., "How coffee consumption varies by profession" instead of generic "Coffee consumption trends")
5. Themes should evoke curiosity or challenge common assumptions
6. Consider timely topics, emerging trends, or evergreen insights

**THEME STYLES TO INCLUDE:**
- "Did you know..." style surprising statistics
- "The real cost of..." economic breakdowns
- "A day/year in the life of..." behavioral patterns
- "X vs Y: The ultimate comparison" head-to-head analysis
- "The rise and fall of..." historical trends
- "What [demographic] really thinks about..." opinion data
- "Behind the numbers of..." deep-dive analysis
- "The geography of..." spatial distributions
- "Before and after..." transformation stories

Return ONLY valid JSON in this exact format:
[
  {{
    "id": 1,
    "theme": "[Specific, engaging theme title that could be an infographic headline]",
    "description": "[One-sentence description of the data story and why it's interesting]"
  }},
  ...
]

Generate {count} DIVERSE themes - avoid repetitive patterns or similar angles. Each theme should feel fresh and distinct.
    """
    
    response = query_llm(prompt)
    if not response:
        return []
    
    # Parse the JSON response with robust cleaning
    try:
        # 清理可能的 markdown 代码块
        cleaned_response = response.strip()
        
        # 如果响应被包裹在代码块中
        if cleaned_response.startswith('```'):
            lines = cleaned_response.split('\n')
            # 移除第一行和最后一行的```
            if lines[-1].strip() == '```' or lines[-1].strip().startswith('```'):
                cleaned_response = '\n'.join(lines[1:-1])
            else:
                cleaned_response = '\n'.join(lines[1:])
            # 进一步清理
            cleaned_response = cleaned_response.replace('```json', '').replace('```', '').strip()
        
        # 尝试提取JSON数组
        json_match = re.search(r'(\[[\s\S]*\])', cleaned_response)
        if json_match:
            json_content = json_match.group(1)
        else:
            json_content = cleaned_response
        
        # 解析JSON
        themes_data = json.loads(json_content)
        
        # 验证返回的数据结构
        if isinstance(themes_data, list) and len(themes_data) > 0:
            # 验证每个主题是否有必需的字段
            valid_themes = []
            for theme in themes_data:
                if isinstance(theme, dict) and 'theme' in theme and 'description' in theme:
                    valid_themes.append(theme)
            
            if valid_themes:
                return valid_themes
            else:
                thread_safe_print(f"⚠️  主题 '{specific_theme}' 的响应缺少必需字段")
                return []
        else:
            thread_safe_print(f"⚠️  主题 '{specific_theme}' 的响应不是有效的列表")
            return []
            
    except json.JSONDecodeError as e:
        thread_safe_print(f"❌ 解析 JSON 失败,主题 '{specific_theme}': {e}")
        thread_safe_print(f"   响应内容(前500字符): {response[:500]}")
        return []
    except Exception as e:
        thread_safe_print(f"❌ 处理响应时出错,主题 '{specific_theme}': {e}")
        return []

def process_specific_theme(main_theme: str, specific_theme_data: Dict, all_results: List[Dict]) -> None:
    """
    Process a specific theme and add generated similar themes to the results
    Args:
        main_theme: The main theme category
        specific_theme_data: Dictionary with number and theme content
        all_results: List to store all results
    """
    specific_theme = specific_theme_data["theme"]
    original_number = specific_theme_data["number"]
    
    thread_safe_print(f"\n{'='*80}")
    thread_safe_print(f"🔄 正在处理主题")
    thread_safe_print(f"   分类: {main_theme}")
    thread_safe_print(f"   主题: {specific_theme}")
    thread_safe_print(f"   编号: {original_number}")
    
    # First add the original theme as the first entry
    with print_lock:
        original_theme_entry = {
            "id": len(all_results) + 1,
            "theme": specific_theme,
            "description": f"Original theme {original_number} from {main_theme} category",
            "main_category": main_theme,
            "is_original": True,
            "original_number": original_number
        }
        all_results.append(original_theme_entry)
    
    thread_safe_print(f"🤖 调用 LLM 生成相似主题...")
    
    # Generate similar themes
    similar_themes = generate_similar_themes(main_theme, specific_theme)
    
    if similar_themes:
        # Add main category and reference to original theme
        for theme in similar_themes:
            theme["main_category"] = main_theme
            theme["is_original"] = False
            theme["related_to_original"] = original_number
        
        with print_lock:
            all_results.extend(similar_themes)
        
        thread_safe_print(f"✅ 成功生成 {len(similar_themes)} 个相似主题")
        thread_safe_print(f"   总进度: {len(all_results)} 个主题已生成")
    else:
        thread_safe_print(f"❌ 生成主题失败: '{specific_theme}'")

def main():
    import time
    
    theme_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "theme.txt")
    output_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "theme_new.json")
    
    print("="*80)
    print("🚀 开始主题生成流程")
    print("="*80)
    print(f"📖 读取主题文件: {theme_file}")
    print(f"💾 输出文件: {output_file}")
    
    # Read the original theme file
    themes = read_theme_file(theme_file)
    
    total_specific_themes = sum(len(specific_themes) for specific_themes in themes.values())
    print(f"✅ 成功读取 {len(themes)} 个主分类,共 {total_specific_themes} 个具体主题")
    print(f"🔧 并行线程数: 4")
    print("="*80)
    
    # Initialize results list for all themes
    all_results = []
    
    start_time = time.time()
    
    # Use a thread pool to process specific themes in parallel
    with ThreadPoolExecutor(max_workers=8) as executor:
        futures = []
        
        for main_theme, specific_themes in themes.items():
            for specific_theme_data in specific_themes:
                future = executor.submit(
                    process_specific_theme, 
                    main_theme, 
                    specific_theme_data, 
                    all_results
                )
                futures.append(future)
        
        # Wait for all tasks to complete
        for future in futures:
            future.result()
    
    elapsed_time = time.time() - start_time
    
    print("\n" + "="*80)
    print("📊 重新分配主题 ID...")
    
    # Reassign IDs to ensure they are sequential across all themes
    for i, theme in enumerate(all_results, 1):
        theme["id"] = i
    
    print(f"💾 保存主题到文件: {output_file}")
    
    # Save all themes to the output file
    with open(output_file, 'w', encoding='utf-8') as f:
        json.dump(all_results, f, indent=2, ensure_ascii=False)
    
    print("\n" + "="*80)
    print("✅ 所有主题处理完成!")
    print("="*80)
    print(f"📈 统计信息:")
    print(f"   总主题数: {len(all_results)}")
    print(f"   原始主题: {total_specific_themes}")
    print(f"   生成主题: {len(all_results) - total_specific_themes}")
    print(f"   扩展比例: {len(all_results) / total_specific_themes:.1f}x")
    print(f"⏱️  总耗时: {elapsed_time:.1f} 秒")
    print(f"💾 保存路径: {output_file}")
    print("="*80)

if __name__ == "__main__":
    main()