#!/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()