Ray1ee01 commited on
Commit
7ae3831
·
verified ·
1 Parent(s): 1b27d6b

Upload folder using huggingface_hub

Browse files
modules/color_recommender/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Color Recommender package initialization.
3
+ """
modules/color_recommender/color_framework.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import colour, json, random, os
2
+ import numpy as np
3
+ from pathlib import Path
4
+ import sys
5
+
6
+ # 获取当前文件的绝对路径
7
+ current_file = Path(__file__).resolve()
8
+
9
+ # 获取项目根目录路径 (假设config.py在根目录)
10
+ project_root = current_file.parent.parent.parent
11
+
12
+ # 将项目根目录添加到Python路径中
13
+ if str(project_root) not in sys.path:
14
+ sys.path.append(str(project_root))
15
+
16
+ from config import sentence_transformer_path, infographic_library_path, infographic_image_path, color_cache_path, test_c2t_root
17
+ from llm_api import ask, ask_image
18
+ from infographic_retrieve import InfographicRetriever
19
+
20
+ ifg_retriever = InfographicRetriever(sentence_transformer_path, infographic_library_path, infographic_image_path)
21
+
22
+ encoding_prompt = """You are a data visualization expert. Based on the provided JSON data description for a chart, recommend appropriate color encodings that best represent the data.
23
+
24
+ Input JSON contains:
25
+ - Chart title, description and main insight
26
+ - Column definitions with names, importance, descriptions and roles (x, y, group, etc.)
27
+ - Data samples
28
+ - Chart type specification
29
+ - Data format details
30
+
31
+ Analyze the data and provide color encoding recommendations in the following JSON format:
32
+
33
+ {
34
+ "color_encoding_options": [
35
+ // Array of possible encoding columns in recommended priority order
36
+ // Each item should be a column role from input JSON or "none"
37
+ // IMPORTANT: Only include "none" if using the same color would NOT result in information loss
38
+ // If different colors are needed to distinguish data points, "none" should not be included
39
+ ],
40
+ "recommendations": [
41
+ // recommended choices for each column
42
+ {
43
+ "color_encoding_option": "column_role",
44
+ "explanation": "Explanation of why this encoding is appropriate",
45
+ "color_scheme": "monochrome|dual-color|colorful",
46
+ "confidence": 0.8
47
+ }
48
+ ]
49
+ }
50
+
51
+ For color schemes:
52
+ - "monochrome": Single color with variations in saturation/lightness
53
+ - "dual-color": Two contrasting colors (e.g., blue/orange)
54
+ - "colorful": Multiple distinct colors
55
+
56
+ Consider:
57
+ 1. Semantic meaning of data (sentiments, categories, etc.)
58
+ 2. Number of distinct groups/categories
59
+ 3. Chart type and visual clarity
60
+
61
+ """
62
+
63
+ append_info = """
64
+ The JSON data is: {json_data}
65
+ """
66
+
67
+ encode_result_case = """```json
68
+ {
69
+ "color_encoding_options": [
70
+ "group"
71
+ ],
72
+ "recommendations": [
73
+ {
74
+ "color_encoding_option": "group",
75
+ "explanation": "Encoding 'Sentiment' with color is crucial for distinguishing between positive, neutral, and negative headline shares within each person's stacked bar. Differentiation is essential to accurately perceive the proportions of each sentiment.",
76
+ "color_scheme": "colorful",
77
+ "confidence": 0.95
78
+ }
79
+ ]
80
+ }
81
+ ```
82
+ """
83
+
84
+ judge_prompt = '''You are an expert in color theory and visual design. You will receive the following information:
85
+ 1. A chart title: {title}
86
+ 2. A brief description of the chart’s subject: {description}
87
+ 3. A list of chart columns or data categories: {columns}
88
+ 4. A palette with following hex color: {colors}
89
+
90
+ Your task is to evaluate how suitable this palette is for the chart based on emotional and thematic appropriateness.
91
+ For instance, if the chart addresses a serious topic, darker or more subdued tones might be more fitting; if the topic is about environmental awareness, shades of green may be preferable.
92
+
93
+ Please provide:
94
+ - An overall suitability score from 1 to 5 (with 5 indicating an excellent match and 1 indicating a poor match) for the palette.
95
+ - Explanation for the score from any issues of emotional or thematic appropriateness.
96
+ '''
97
+
98
+ judge_return_case = '''
99
+
100
+ Return your assessment in plain text, following the json format:
101
+ {
102
+ "score": 4,
103
+ "Explanation": "The palette is well-suited for the chart's subject matter, with a range of colors that are both visually appealing and thematically appropriate. The colors are bright and engaging, which is ideal for a chart that aims to capture the reader's attention."
104
+ }
105
+
106
+ Thank you!'''
107
+
108
+
109
+
110
+ cache_color_results = None
111
+ def load_cache_results(indexes):
112
+ global cache_color_results
113
+ if cache_color_results is None:
114
+ with open(color_cache_path, 'r') as f:
115
+ cache_color_results = json.load(f)
116
+ return [cache_color_results[i] for i in indexes]
117
+
118
+ class ColorFramework(object):
119
+ def __init__(self, data_info):
120
+ self.data_info = data_info
121
+ self.color_schemes = self.__get_color_encoding() # TODO: may add error handling
122
+
123
+ def __get_color_encoding(self):
124
+ # return dict if success, None if failed
125
+ prompt = encoding_prompt + append_info.format(json_data=self.data_info)
126
+ response = ask(prompt)
127
+ # response = encode_result_case
128
+ try:
129
+ response = json.loads(response)
130
+ except Exception as e:
131
+ response = response.split('```json')[1].split('```')[0]
132
+ response = json.loads(response)
133
+ print(response)
134
+
135
+ options = response['color_encoding_options']
136
+ recommendations = response['recommendations']
137
+ res = []
138
+ for recomd in recommendations:
139
+ option = recomd['color_encoding_option']
140
+ column_id = -2
141
+ if option == 'none':
142
+ column_id = -1
143
+ else:
144
+ for i, column in enumerate(self.data_info['columns']):
145
+ if column['role'] == option:
146
+ column_id = i
147
+ break
148
+ if column_id == -2:
149
+ print('No column name existed')
150
+ return None
151
+ res.append({
152
+ 'column_id': column_id,
153
+ 'column_name': self.data_info['columns'][column_id]['name'],
154
+ 'column_role': option,
155
+ 'color_scheme': recomd['color_scheme'],
156
+ 'confidence': recomd['confidence']
157
+ })
158
+ return res
159
+
160
+ def load_scheme_list(self):
161
+ return self.color_schemes
162
+
163
+ def get_infographic_palette(self, scheme_id, topk_num=20, query_threshold=0.3, valid_threshold=3.0, random_seed=-1):
164
+ # return list if success, None if failed
165
+ scheme = self.color_schemes[scheme_id]
166
+ search_query = f"{self.data_info['title']} {self.data_info['description']} {self.data_info['main_insight']} {self.data_info['columns'][scheme['column_id']]['name']}"
167
+ # 1. first retrieve by semantic
168
+ results = ifg_retriever.retrieve_similar_entries(search_query, top_k=topk_num)
169
+ max_similarity = results[0][1]
170
+ results_f = [r for r in results if r[1] >= query_threshold * max_similarity]
171
+
172
+ # 2. filter color mode
173
+ results_ids = [r[2] for r in results_f]
174
+ palettes = load_cache_results(results_ids)
175
+ pkg_palettes = zip(results_f, palettes)
176
+ filter_palettes = [palette for palette in pkg_palettes if palette[1]['mode'] == scheme['color_scheme']]
177
+ # print(len(filter_palettes))
178
+ # print(filter_palettes)
179
+ # print(results_f)
180
+ print('filter_palettes:', filter_palettes)
181
+
182
+ # 3. judge the palette score
183
+ scores = []
184
+ for palette in filter_palettes:
185
+ prompt = judge_prompt.format(title=self.data_info['title'], description=self.data_info['description'], columns=self.data_info['columns'], colors=palette[1]['main_color']) + judge_return_case
186
+ # print(prompt); exit()
187
+ response = ask(prompt)
188
+ # print(response)
189
+ try:
190
+ try:
191
+ response = json.loads(response)
192
+ except Exception as e:
193
+ # from IPython import embed
194
+ # embed()
195
+ response = response.split('```json')[1].split('```')[0]
196
+ response = json.loads(response)
197
+ except:
198
+ response = {'score': 0, 'Explanation': 'Error'}
199
+ score = response['score']
200
+ scores.append(score)
201
+
202
+ conclusion = zip(filter_palettes, scores)
203
+ valid_palettes = [c[0] for c in conclusion if c[1] >= valid_threshold]
204
+ print('find suitable results:', len(valid_palettes))
205
+ if len(valid_palettes) == 0:
206
+ print('No suitable palette found')
207
+ return None
208
+ # save all valid palettes
209
+ if random_seed == -1:
210
+ return valid_palettes[0][1]
211
+ else:
212
+ random.seed(random_seed)
213
+ return random.choice(valid_palettes)[1]
214
+
215
+
216
+ def test_ColorFramework():
217
+ data_file = os.path.join(test_c2t_root, "127.json")
218
+ data_info = json.load(open(data_file, 'r'))
219
+ # 1. input data_info
220
+ cf = ColorFramework(data_info)
221
+ # print(cf.data_info)
222
+ print(cf.get_infographic_palette(0))
223
+
224
+ # {'mode': 'colorful', 'color_list': ['#4ECBEE', '#FFB51F', '#DC776A', '#3465B1'], 'main_color': ['#4ECBEE', '#FFB51F', '#DC776A', '#3465B1'], 'bcg': '#F2EDEE', 'context_colors': ['#AFAFAF', '#86988B', '#EFDEBA', '#596873', '#2F4455'], 'similar_to_bcg': [], 'other_colors': []}
modules/color_recommender/color_index_builder.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import numpy as np
3
+ import faiss
4
+ from typing import Dict, List, Optional
5
+ import os
6
+ from utils.model_loader import ModelLoader
7
+
8
+ class ColorIndexBuilder:
9
+ def __init__(self, data_path: str = "./static/color_palette.json", index_path: str = "./static/color_palette.index", embed_model_path: str = "all-MiniLM-L6-v2"):
10
+ # Convert relative path to absolute path
11
+ current_dir = os.path.dirname(os.path.abspath(__file__))
12
+ self.color_palette_path = data_path
13
+ # Use the global ModelLoader to get the model instance
14
+ self.model = ModelLoader.get_model(embed_model_path)
15
+ self.index = None
16
+ self.color_palettes = None
17
+ self.index_path = index_path
18
+ self.dimension = 384 # Dimension of the sentence transformer embeddings
19
+
20
+ def load_color_palettes(self) -> Dict[str, Dict]:
21
+ """Load color palettes from the JSON file."""
22
+ with open(self.color_palette_path, 'r', encoding='utf-8') as f:
23
+ return json.load(f)
24
+
25
+ def create_text_for_embedding(self, palette: Dict) -> str:
26
+ """Create a text string from palette metadata for embedding."""
27
+ text_parts = []
28
+
29
+ # Add text if available
30
+ if 'text' in palette:
31
+ text_parts.append(palette['text'])
32
+
33
+ # Add facts if available
34
+ if 'facts' in palette:
35
+ facts = palette['facts']
36
+ if isinstance(facts, list):
37
+ text_parts.extend(facts)
38
+ elif isinstance(facts, str):
39
+ text_parts.append(facts)
40
+
41
+ # Add columns if available
42
+ if 'columns' in palette:
43
+ columns = palette['columns']
44
+ if isinstance(columns, list):
45
+ text_parts.extend(columns)
46
+ elif isinstance(columns, str):
47
+ text_parts.append(columns)
48
+
49
+ return ' '.join(text_parts)
50
+
51
+ def build_index(self):
52
+ """Build the FAISS index for color palettes."""
53
+ # Load color palettes
54
+ self.color_palettes = self.load_color_palettes()
55
+
56
+ # Create embeddings for each palette
57
+ embeddings = []
58
+ self.palette_indices = []
59
+
60
+ for index, palette in self.color_palettes.items():
61
+ text = self.create_text_for_embedding(palette)
62
+ print('text', text)
63
+ embedding = self.model.encode(text)
64
+ embeddings.append(embedding)
65
+ self.palette_indices.append(index)
66
+
67
+ # Convert to numpy array
68
+ embeddings = np.array(embeddings).astype('float32')
69
+
70
+ # Create and train the index
71
+ self.index = faiss.IndexFlatL2(self.dimension)
72
+ self.index.add(embeddings)
73
+
74
+ def find_similar_palettes(self, query_text: str, k: int = 5) -> List[Dict]:
75
+ """
76
+ Find similar color palettes based on text query.
77
+
78
+ Args:
79
+ query_text: Text to search for similar palettes
80
+ k: Number of similar palettes to return
81
+
82
+ Returns:
83
+ List of similar color palettes with their distances
84
+ """
85
+ if self.index is None:
86
+ self.build_index()
87
+
88
+ # Create embedding for query
89
+ query_embedding = self.model.encode(query_text)
90
+ query_embedding = np.array([query_embedding]).astype('float32')
91
+
92
+ # Search the index
93
+ distances, indices = self.index.search(query_embedding, k)
94
+
95
+ # Return similar palettes with their distances
96
+ results = []
97
+ for i, idx in enumerate(indices[0]):
98
+ if idx < len(self.palette_indices): # Ensure index is valid
99
+ palette_index = self.palette_indices[idx]
100
+ results.append({
101
+ 'palette': self.color_palettes[palette_index],
102
+ 'distance': float(distances[0][i])
103
+ })
104
+
105
+ return results
106
+
107
+ def save_index(self, output_path: str):
108
+ """Save the FAISS index to disk."""
109
+ if self.index is None:
110
+ raise ValueError("Index has not been built yet")
111
+ # Convert relative path to absolute path
112
+ #current_dir = os.path.dirname(os.path.abspath(__file__))
113
+ abs_output_path = output_path
114
+ faiss.write_index(self.index, abs_output_path)
115
+
116
+ # Save palette indices mapping
117
+ indices_path = output_path + ".indices"
118
+ with open(indices_path, 'w', encoding='utf-8') as f:
119
+ json.dump(self.palette_indices, f)
120
+
121
+ def load_index(self):
122
+ """Load a FAISS index from disk."""
123
+ # Convert relative path to absolute path
124
+ # current_dir = os.path.dirname(os.path.abspath(__file__))
125
+ index_path = self.index_path
126
+ self.index = faiss.read_index(index_path)
127
+
128
+ # Load color palettes
129
+ self.color_palettes = self.load_color_palettes()
130
+
131
+ # Load palette indices mapping
132
+ indices_path = index_path + ".indices"
133
+ if os.path.exists(indices_path):
134
+ with open(indices_path, 'r', encoding='utf-8') as f:
135
+ self.palette_indices = json.load(f)
136
+ else:
137
+ # For backward compatibility, create indices from keys
138
+ self.palette_indices = list(self.color_palettes.keys())
modules/color_recommender/color_recommender.py ADDED
@@ -0,0 +1,760 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Dict, List, Optional, Union
3
+ import pandas as pd
4
+ import random
5
+ import argparse
6
+ import os
7
+ from copy import deepcopy
8
+ from logging import getLogger
9
+ from modules.color_recommender.color_index_builder import ColorIndexBuilder
10
+
11
+ import numpy as np
12
+ if not hasattr(np, "asscalar"):
13
+ np.asscalar = lambda x: x.item()
14
+
15
+ from colormath.color_objects import sRGBColor, LabColor
16
+ from colormath.color_conversions import convert_color
17
+ from colormath.color_diff import delta_e_cie2000
18
+ from itertools import combinations
19
+
20
+ def hex_to_lab(hex_color):
21
+ """HEX -> LAB"""
22
+ hex_color = hex_color.lstrip("#")
23
+ r = int(hex_color[0:2], 16)
24
+ g = int(hex_color[2:4], 16)
25
+ b = int(hex_color[4:6], 16)
26
+
27
+ rgb = sRGBColor(r, g, b, is_upscaled=True)
28
+ lab = convert_color(rgb, LabColor)
29
+ return lab
30
+
31
+ def color_distance(hex1, hex2):
32
+ """计算两个颜色的 ΔE (CIEDE2000)"""
33
+ lab1 = hex_to_lab(hex1)
34
+ lab2 = hex_to_lab(hex2)
35
+ return delta_e_cie2000(lab1, lab2)
36
+
37
+ def mark_color_separation(colors):
38
+ """
39
+ 计算所有 mark 颜色之间的区分度
40
+ """
41
+ results = []
42
+ for c1, c2 in combinations(colors, 2):
43
+ d = color_distance(c1, c2)
44
+ results.append({
45
+ "color1": c1,
46
+ "color2": c2,
47
+ "deltaE": round(d, 2)
48
+ })
49
+ minD = 1000
50
+ for item in results:
51
+ minD = min(minD, item["deltaE"])
52
+ return results, minD
53
+
54
+ def srgb_to_linear(c):
55
+ """sRGB → 线性RGB"""
56
+ c = c / 255.0
57
+ if c <= 0.04045:
58
+ return c / 12.92
59
+ else:
60
+ return ((c + 0.055) / 1.055) ** 2.4
61
+
62
+ def relative_luminance(hex_color):
63
+ """计算WCAG相对亮度"""
64
+ r, g, b = hex_to_rgb(hex_color)
65
+ r_lin = srgb_to_linear(r*255)
66
+ g_lin = srgb_to_linear(g*255)
67
+ b_lin = srgb_to_linear(b*255)
68
+ # print((r, g, b), (r_lin, g_lin, b_lin))
69
+
70
+ return 0.2126 * r_lin + 0.7152 * g_lin + 0.0722 * b_lin
71
+
72
+ def contrast_ratio(color1, color2):
73
+ """计算WCAG对比度"""
74
+ l1 = relative_luminance(color1)
75
+ l2 = relative_luminance(color2)
76
+
77
+ lighter = max(l1, l2)
78
+ darker = min(l1, l2)
79
+
80
+ # print(color1, color2, lighter, darker, (lighter + 0.05) / (darker + 0.05))
81
+
82
+ return (lighter + 0.05) / (darker + 0.05)
83
+
84
+ def rgb_to_hex(r, g, b):
85
+ r = max(0, min(int(r), 255))
86
+ g = max(0, min(int(g), 255))
87
+ b = max(0, min(int(b), 255))
88
+ return '#{:02x}{:02x}{:02x}'.format(r, g, b)
89
+
90
+ import colorsys
91
+
92
+ # 复用之前的函数
93
+ def adapt_color_for_dark_background(color, brightness_adjustment=25, saturation_adjustment=15,
94
+ preserve_grays=True, min_brightness=60, max_brightness=85,
95
+ contrast_enhancement=10):
96
+ """将适用于浅色背景的颜色转换为适用于深色背景的颜色"""
97
+ rgb = hex_to_rgb(color)
98
+ h, s, l = rgb_to_hsl(*rgb)
99
+
100
+ perceived_brightness = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2])
101
+ if preserve_grays and s < 0.1:
102
+ s = min(0.15, s + 0.1) # 增加一些饱和度,但不超过15%
103
+ l = max(min_brightness/100, l + brightness_adjustment/100) # 显著提高亮度
104
+ else:
105
+ # 动态调整饱和度 - 根据原始亮度调整
106
+ dynamic_sat_adjustment = saturation_adjustment * (1 - perceived_brightness) * 1.5
107
+
108
+ s = max(0, min(1, s + dynamic_sat_adjustment/100))
109
+ dynamic_brightness_adjustment = brightness_adjustment * (1 - perceived_brightness) * 1.5
110
+ l = max(0, min(1, l + dynamic_brightness_adjustment/100))
111
+
112
+ # 增强对比度
113
+ if l < 0.5:
114
+ contrast_factor = contrast_enhancement / 200
115
+ l = max(0, l - contrast_factor * (0.5 - l))
116
+ else:
117
+ contrast_factor = contrast_enhancement / 200
118
+ l = min(1, l + contrast_factor * (l - 0.5))
119
+
120
+ l = max(min_brightness/100, l)
121
+ l = min(max_brightness/100, l)
122
+
123
+ hsl_str = f"hsl({int(h*360)}°, {int(s*100)}%, {int(l*100)}%)"
124
+ hex_str = hsl_to_hex(h, s, l)
125
+
126
+ return hex_str
127
+ def adapt_color_for_light_background(color, brightness_reduction=15, saturation_adjustment=5,
128
+ preserve_grays=True, min_brightness=25, max_brightness=90, # Increased max_brightness
129
+ contrast_enhancement=10):
130
+ """
131
+ 将颜色优化为适用于浅色背景的颜色
132
+
133
+ 参数:
134
+ color (str): 颜色值,支持十六进制格式(#RGB 或 #RRGGBB)
135
+ brightness_reduction (int): 亮度降低百分比 (0-100)
136
+ saturation_adjustment (int): 饱和度调整百分比 (-100 到 100)
137
+ preserve_grays (bool): 是否保持灰色调的特殊处理
138
+ min_brightness (int): 最小亮度阈值 (0-100)
139
+ max_brightness (int): 最大亮度阈值 (0-100)
140
+ contrast_enhancement (int): 对比度增强系数 (0-100)
141
+
142
+ 返回:
143
+ tuple: 转换后的HSL颜色字符串和十六进制颜色字符串的元组
144
+ """
145
+ # ��十六进制颜色转换为RGB
146
+ rgb = hex_to_rgb(color)
147
+ h, s, l = rgb_to_hsl(*rgb)
148
+ perceived_brightness = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2])
149
+
150
+ if preserve_grays and s < 0.1:
151
+ # 灰色调在浅色背景上需要足够深,但不要太黑
152
+ l = max(min_brightness/100, min(0.6, l - 0.1)) # Adjusted to make it lighter
153
+ s = min(0.05, s) # 保持低饱和度
154
+ else:
155
+ if l > 0.7:
156
+ dynamic_brightness_reduction = brightness_reduction * (perceived_brightness)
157
+ l = max(min_brightness/100, l - dynamic_brightness_reduction/100)
158
+
159
+ if l < 0.3:
160
+ l = min(0.5, l + 0.1) # Adjusted to make it lighter
161
+
162
+ if s < 0.4 and l > 0.5:
163
+ s = min(0.6, s + saturation_adjustment/100 * 2)
164
+ elif s > 0.8 and l < 0.4:
165
+ s = max(0.6, s - saturation_adjustment/100)
166
+
167
+ if l > 0.65:
168
+ contrast_factor = contrast_enhancement / 200
169
+ l = max(min_brightness/100, l - contrast_factor * (l - 0.5))
170
+ elif l < 0.45:
171
+ contrast_factor = contrast_enhancement / 300
172
+ l = min(0.6, l + contrast_factor * (0.5 - l)) # Adjusted to make it lighter
173
+
174
+ l = max(min_brightness/100, l)
175
+ l = min(max_brightness/100, l)
176
+
177
+ hsl_str = f"hsl({int(h*360)}°, {int(s*100)}%, {int(l*100)}%)"
178
+ hex_str = hsl_to_hex(h, s, l)
179
+
180
+ return hex_str
181
+
182
+ def convert_palette_for_light_background(palette, **kwargs):
183
+ """
184
+ 转换整个调色板以适应浅色背景
185
+
186
+ 参数:
187
+ palette (list): 颜色列表,每个颜色为十六进制格式
188
+ **kwargs: 传递给adapt_color_for_light_background的选项
189
+
190
+ 返回:
191
+ list: 转换后的颜色列表,每个元素为(hsl, hex)的元组
192
+ """
193
+ converted_palette = []
194
+
195
+ for color in palette:
196
+ converted = adapt_color_for_light_background(color, **kwargs)
197
+ converted_palette.append(converted)
198
+
199
+ return converted_palette
200
+
201
+ def enhance_palette_accessibility(palette, background_is_dark=True, **kwargs):
202
+ """
203
+ 根据背景色增强调色板的可访问性
204
+
205
+ 参数:
206
+ palette (list): 颜色列表,每个颜色为十六进制格式
207
+ background_is_dark (bool): 背景是否为深色
208
+ **kwargs: 配置选项
209
+
210
+ 返回:
211
+ list: 转换后的颜色列表,每个元素为(hsl, hex)的元组
212
+ """
213
+ if background_is_dark:
214
+ return convert_palette_for_dark_background(palette, **kwargs)
215
+ else:
216
+ return convert_palette_for_light_background(palette, **kwargs)
217
+
218
+ def hex_to_rgb(hex_color):
219
+ """将十六进制颜色转换为RGB"""
220
+ hex_color = hex_color.lstrip('#')
221
+ if len(hex_color) == 3:
222
+ hex_color = ''.join([c + c for c in hex_color])
223
+
224
+ r = int(hex_color[0:2], 16) / 255.0
225
+ g = int(hex_color[2:4], 16) / 255.0
226
+ b = int(hex_color[4:6], 16) / 255.0
227
+
228
+ return (r, g, b)
229
+
230
+ def rgb_to_hsl(r, g, b):
231
+ """将RGB转换为HSL"""
232
+ h, l, s = colorsys.rgb_to_hls(r, g, b)
233
+ return (h, s, l)
234
+
235
+ def hsl_to_hex(h, s, l):
236
+ """将HSL转换为十六进制颜色"""
237
+ r, g, b = colorsys.hls_to_rgb(h, l, s)
238
+
239
+ r = int(r * 255)
240
+ g = int(g * 255)
241
+ b = int(b * 255)
242
+
243
+ return f"#{r:02x}{g:02x}{b:02x}"
244
+
245
+ def convert_palette_for_dark_background(palette, **kwargs):
246
+ """
247
+ 转换整个调色板以适应深色背景
248
+
249
+ 参数:
250
+ palette (list): 颜色列表,每个颜色为十六进制格式
251
+ **kwargs: 传递给adapt_color_for_dark_background的选项
252
+
253
+ 返回:
254
+ list: 转换后的颜色列表,每个元素为(hsl, hex)的元组
255
+ """
256
+ converted_palette = []
257
+
258
+ for color in palette:
259
+ converted = adapt_color_for_dark_background(color, **kwargs)
260
+ converted_palette.append(converted)
261
+
262
+ return converted_palette
263
+
264
+
265
+ fixed_color_palette = [
266
+ ["#4269d0","#efb118","#ff725c","#6cc5b0","#3ca951","#ff8ab7","#a463f2","#97bbf5","#9c6b4e","#9498a0"],
267
+ ["#4e79a7","#f28e2c","#e15759","#76b7b2","#59a14f","#edc949","#af7aa1","#ff9da7","#9c755f","#bab0ab"],
268
+ ["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f","#e5c494","#b3b3b3"],
269
+ ]
270
+
271
+ # (bg_hex, text_hex) pairs — text is a dark tone that harmonizes with the bg hue
272
+ light_background_palette = [
273
+ # neutral / warm-white
274
+ ("#F5F5F0", "#2C2C28"), ("#F3F3F3", "#2A2A2A"), ("#EAEAEA", "#282828"),
275
+ ("#F0EEE8", "#2C2A22"), ("#F2F2E9", "#2A2A20"), ("#F3ECD9", "#332B18"),
276
+ ("#ECE3D9", "#32281E"), ("#EFE7D8", "#332A1C"), ("#E6E3D9", "#302A20"),
277
+ # warm: peach / sand / butter
278
+ ("#F7EDE2", "#3D2510"), ("#F5E6D3", "#3A2210"), ("#F2DFC8", "#3A2010"),
279
+ ("#F9F0DC", "#3A3010"), ("#F5EDD0", "#38300E"), ("#F2E8C4", "#362E0C"),
280
+ ("#FAF0E6", "#3C2C14"), ("#F8E8D8", "#3A2818"),
281
+ # warm: blush / rose
282
+ ("#F5E8E8", "#3D1818"), ("#F2E0E0", "#3A1616"), ("#F0D8D8", "#381414"),
283
+ ("#F7EAEA", "#3C1A1A"), ("#EFE0E4", "#38161C"),
284
+ # cool: sky / slate-blue
285
+ ("#E8F0F7", "#18283D"), ("#E2EBF5", "#162438"), ("#DCE8F2", "#142030"),
286
+ ("#E6EEF8", "#1A2A3C"), ("#E0EAF5", "#162238"), ("#D8E8F0", "#121E2E"),
287
+ ("#E8F4F8", "#183040"),
288
+ # cool: mint / sage
289
+ ("#E6F2EE", "#183028"), ("#DFF0EA", "#142C24"), ("#E0EDE8", "#182A22"),
290
+ ("#E8F5F0", "#1A3228"), ("#D8EDE6", "#102820"),
291
+ # cool: lavender / lilac
292
+ ("#EEE8F5", "#2A1840"), ("#EAE2F5", "#261638"), ("#E8E0F0", "#221430"),
293
+ ("#F0EAF7", "#2C1A42"), ("#ECE4F2", "#28163C"),
294
+ # cool: soft-teal
295
+ ("#E0F0F2", "#10282C"), ("#D8EEF0", "#0E2428"), ("#E4F2F4", "#122A2E"),
296
+ # warm-green / olive
297
+ ("#EAF0E0", "#222C10"), ("#E8EDD8", "#202A0E"), ("#E4EAD4", "#1E280C"),
298
+ ("#EDF2E2", "#242E12"),
299
+ # medium-toned (slightly more saturated, still light)
300
+ ("#DDE8F0", "#122030"), ("#D8E8E0", "#102820"), ("#E8DDF0", "#201030"),
301
+ ("#F0DDE8", "#301020"), ("#F0E8DD", "#302010"),
302
+ ]
303
+
304
+ light_background_colors = [bg for bg, _ in light_background_palette]
305
+
306
+
307
+ def _text_color_for_bg(bg_hex: str) -> str:
308
+ """Return a harmonized dark text color for the given light background."""
309
+ for bg, text in light_background_palette:
310
+ if bg.upper() == bg_hex.upper():
311
+ return text
312
+ # fallback: use luminance-derived neutral dark
313
+ try:
314
+ r, g, b = int(bg_hex[1:3], 16), int(bg_hex[3:5], 16), int(bg_hex[5:7], 16)
315
+ # shift hue toward darker version of the bg
316
+ h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255)
317
+ v2 = max(0.12, v - 0.68)
318
+ s2 = min(1.0, s * 1.2)
319
+ r2, g2, b2 = colorsys.hsv_to_rgb(h, s2, v2)
320
+ return rgb_to_hex(int(r2 * 255), int(g2 * 255), int(b2 * 255))
321
+ except Exception:
322
+ return "#2a2a2a"
323
+
324
+ dark_background_colors = [
325
+ "#1e2130", "#1a1a2e", "#191919", "#162447",
326
+ "#2d3142", "#1f2b44", "#2b2b3d", "#282828", "#243447",
327
+ "#373e57", "#343434", "#203a43", "#1c2541", "#2c2c34",
328
+ "#2e2e2e", "#254362", "#352f44", "#204051", "#303030"
329
+ ]
330
+
331
+ class ColorRecommender:
332
+ def __init__(self, embed_model_path: str = "all-MiniLM-L6-v2", data_path: str = None, index_path: str = None):
333
+ self.color_schemes = {}
334
+ self.index_builder = ColorIndexBuilder(embed_model_path=embed_model_path, data_path=data_path, index_path=index_path)
335
+ self.index_builder.load_index()
336
+ black = (21, 21, 21)
337
+ white = (240, 240, 240)
338
+ gray1 = (75, 75, 75)
339
+ gray2 = (150, 150, 150)
340
+ gray3 = (200, 200, 200)
341
+ self.basic_colors = [black, white, gray1, gray2, gray3]
342
+ self.basic_colors_hex = [rgb_to_hex(*color) for color in self.basic_colors]
343
+
344
+ def create_query_text(self, input_data: Dict) -> str:
345
+ """Create a text query from input data for finding similar color palettes."""
346
+ data_dict = input_data.get("data", {})
347
+ columns = data_dict.get("columns", [])
348
+ metadata = input_data.get("metadata", {})
349
+
350
+ text_parts = []
351
+
352
+ # Add column information
353
+ column_texts = []
354
+ for col in columns:
355
+ col_text = f"{col['name']} ({col['data_type']})"
356
+ if 'description' in col:
357
+ col_text += f": {col['description']}"
358
+ column_texts.append(col_text)
359
+ text_parts.append(" ".join(column_texts))
360
+
361
+ # Add metadata
362
+ if 'title' in metadata:
363
+ text_parts.append(metadata['title'])
364
+ if 'description' in metadata:
365
+ text_parts.append(metadata['description'])
366
+ if 'main_insight' in metadata:
367
+ text_parts.append(metadata['main_insight'])
368
+
369
+ return " ".join(text_parts)
370
+
371
+ def determine_by_group(self, columns: List[Dict], combination: str) -> Optional[str]:
372
+ """
373
+ Determine which column to use for grouping based on the combination type.
374
+
375
+ Args:
376
+ columns: List of column dictionaries containing name and data_type
377
+ combination: Type of combination (e.g., "categorical + numerical")
378
+
379
+ Returns:
380
+ The column name to use for grouping, or None if no grouping should be used
381
+ """
382
+ column_names = [col["name"] for col in columns]
383
+ if combination == "categorical + numerical" or combination == "categorical + numerical + numerical":
384
+ return column_names[0]
385
+ elif combination == "categorical + numerical + categorical":
386
+ return column_names[2]
387
+ elif combination == "temporal + numerical + categorical":
388
+ return column_names[2]
389
+ elif combination == "temporal + numerical":
390
+ return column_names[0]
391
+ elif combination == "categorical + numerical + temporal":
392
+ return column_names[0]
393
+ else:
394
+ ret = None
395
+ for col in columns:
396
+ if col["data_type"] == "categorical" or col["data_type"] == "temporal":
397
+ ret = col["name"]
398
+ return ret
399
+
400
+ def should_color_by_group(self, by_group: str, data: pd.DataFrame, columns: List[Dict]) -> bool:
401
+ """
402
+ Determine whether to use color grouping based on the column's unique values and type.
403
+
404
+ Args:
405
+ by_group: Column name to check
406
+ data: DataFrame containing the data
407
+ columns: List of column dictionaries containing name and data_type
408
+
409
+ Returns:
410
+ True if color grouping should be used, False otherwise
411
+ """
412
+ if by_group is None:
413
+ return False
414
+
415
+ unique_values = data[by_group].nunique()
416
+ column_type = next((col["data_type"] for col in columns if col["name"] == by_group), "")
417
+
418
+ if column_type == "temporal" and unique_values >= 5:
419
+ return False
420
+ elif column_type == "categorical" and unique_values >= 8:
421
+ return False
422
+
423
+ return True
424
+
425
+ def get_required_color_num(self, by_group: Optional[str], data: pd.DataFrame) -> int:
426
+ """
427
+ Calculate the number of colors required based on grouping.
428
+
429
+ Args:
430
+ by_group: Column name used for grouping
431
+ data: DataFrame containing the data
432
+
433
+ Returns:
434
+ Number of colors required
435
+ """
436
+ if by_group is None:
437
+ return 1
438
+ return data[by_group].nunique()
439
+
440
+ def select_suitable_palettes(self, similar_palettes: List[Dict], required_color_num: int) -> Dict:
441
+ """
442
+ Select a suitable palette from similar palettes based on color count requirements.
443
+
444
+ Args:
445
+ similar_palettes: List of similar palettes with their distances
446
+ required_color_num: Number of colors required
447
+
448
+ Returns:
449
+ Selected palette dictionary
450
+ """
451
+ # Filter palettes that have enough colors
452
+ suitable_palettes = [
453
+ p for p in similar_palettes
454
+ if 'main_color' in p['palette'] and len(p['palette']['main_color']) >= required_color_num + 1 and len(p['palette']['main_color']) <= required_color_num + 3
455
+ ]
456
+
457
+ if len(suitable_palettes) == 0:
458
+ # If no palette has enough colors, use the most similar one
459
+ # return random.choice(similar_palettes[:3])['palette']
460
+ return [similar_palette['palette'] for similar_palette in similar_palettes[:3]]
461
+
462
+ # TODO: Implement custom selection logic based on requirements
463
+ # return random.choice(suitable_palettes[:3])['palette']
464
+ return [suitable_palette['palette'] for suitable_palette in suitable_palettes[:3]]
465
+
466
+ def recommend_colors(self, input_data: Dict) -> Dict:
467
+ """
468
+ Recommend colors based on the input data.
469
+
470
+ Args:
471
+ input_data: Dictionary containing the input data
472
+
473
+ Returns:
474
+ Dictionary containing the recommended color scheme
475
+ """
476
+ # Extract necessary information from input_data
477
+ data_dict = input_data.get("data", {})
478
+ data = pd.DataFrame(data_dict.get("data", []))
479
+ columns = data_dict.get("columns", [])
480
+ combination = data_dict.get("type_combination", "")
481
+
482
+ # Step 1: Determine by_group
483
+ by_group = self.determine_by_group(columns, combination)
484
+
485
+ # Step 2: Check if we should color by group
486
+ should_group = self.should_color_by_group(by_group, data, columns)
487
+ if not should_group:
488
+ by_group = None
489
+
490
+ # Step 3: Get required number of colors
491
+ required_color_num = self.get_required_color_num(by_group, data)
492
+
493
+ # Step 4: Find similar color palettes
494
+ query_text = self.create_query_text(input_data)
495
+ similar_palettes = self.index_builder.find_similar_palettes(query_text, k=25)
496
+ # Step 5: Select a suitable palette
497
+ selected_palettes = self.select_suitable_palettes(similar_palettes, required_color_num)
498
+ if not selected_palettes or len(selected_palettes) == 0:
499
+ selected_palettes = [None]
500
+
501
+ color_schemes = []
502
+ for selected_palette in selected_palettes:
503
+ if not selected_palette or len(selected_palette["main_color"]) <= required_color_num:
504
+ scheme = fixed_color_palette[random.randint(0, len(fixed_color_palette) - 1)]
505
+ new_selected_palette = {
506
+ "mode": "monochrome",
507
+ "color_list": scheme,
508
+ "main_color": scheme,
509
+ "num_of_colors": len(scheme),
510
+ "bcg": selected_palette["bcg"] if selected_palette else random.choice(light_background_colors),
511
+ "context_colors": selected_palette["context_colors"] if selected_palette else [],
512
+ "similar_to_bcg": selected_palette["similar_to_bcg"] if selected_palette else 0,
513
+ }
514
+ selected_palette = new_selected_palette
515
+
516
+ # Step 6: Create the color scheme
517
+ chosen_bg = random.choice(light_background_colors)
518
+ color_scheme = {
519
+ "field": {},
520
+ "other": {
521
+ "primary": None,
522
+ },
523
+ "available_colors": [],
524
+ "background_color": chosen_bg,
525
+ "text_color": _text_color_for_bg(chosen_bg)
526
+ }
527
+
528
+ colors = selected_palette["main_color"] + selected_palette["context_colors"]
529
+ if by_group:
530
+ unique_values = data[by_group].unique()
531
+ first_numerical_value = None
532
+ for col in columns:
533
+ if col["data_type"] == "numerical":
534
+ first_numerical_value = col["name"]
535
+ break
536
+ unique_values = [(value, data[first_numerical_value][data[by_group] == value].mean()) for value in unique_values]
537
+ unique_values.sort(key=lambda x: -x[1])
538
+ unique_values = [value[0] for value in unique_values]
539
+ color_scheme["other"]["primary"] = selected_palette["main_color"][0]
540
+ for i, value in enumerate(unique_values):
541
+ color_scheme["field"][str(value)] = selected_palette["main_color"][i]
542
+ if required_color_num < len(colors):
543
+ color_scheme["other"]["secondary"] = colors[required_color_num]
544
+ for i in range(required_color_num + 1, len(colors)):
545
+ color_scheme["available_colors"].append(colors[i])
546
+ else:
547
+ required_color_num = 1
548
+ color_scheme["other"]["primary"] = selected_palette["main_color"][0]
549
+ if required_color_num < len(colors):
550
+ color_scheme["other"]["secondary"] = colors[required_color_num]
551
+ for i in range(required_color_num + 1, len(colors)):
552
+ color_scheme["available_colors"].append(colors[i])
553
+
554
+ color_schemes.append(color_scheme)
555
+
556
+ return color_schemes
557
+
558
+ def process(input: str, output: str, embed_model_path: str = "all-MiniLM-L6-v2", base_url: str = None, api_key: str = None, data_path: str = None, index_path: str = None) -> bool:
559
+ """
560
+ Pipeline入口函数,处理单个文件的颜色推荐
561
+
562
+ Args:
563
+ input_path: 输入JSON文件路径
564
+ output_path: 输出JSON文件路径
565
+ embed_model_path: 嵌入模型路径
566
+ """
567
+ print(f"Processing {input} to {output}")
568
+ try:
569
+ # 读取输入文件
570
+ with open(input, "r", encoding="utf-8") as f:
571
+ data = json.load(f)
572
+
573
+ # 预处理数据,确保类型正确
574
+ processed_data = preprocess_data(data)
575
+
576
+ # 生成颜色推荐
577
+ recommender = ColorRecommender(embed_model_path=embed_model_path, data_path=data_path, index_path=index_path)
578
+ color_results = recommender.recommend_colors(processed_data)
579
+
580
+ color_results2 = []
581
+ for color_result in color_results:
582
+ lighter_color_result = deepcopy(color_result)
583
+ for k, v in lighter_color_result.items():
584
+ if isinstance(v, list):
585
+ lighter_color_result[k] = convert_palette_for_light_background(v)
586
+ elif isinstance(v, dict):
587
+ for k2, v2 in v.items():
588
+ lighter_color_result[k][k2] = convert_palette_for_light_background([v2])[0]
589
+ if "other" in k2.lower() and len(k2) <= 7:
590
+ lighter_color_result[k][k2] = "#6f6f6f"
591
+ elif isinstance(v, str) and k != "background_color":
592
+ lighter_color_result[k] = convert_palette_for_light_background([v])[0]
593
+ # elif k == "background_color":
594
+ # darker_color_result[k] = random.choice(light_background_colors)
595
+
596
+ darker_color_result = deepcopy(color_result)
597
+ for k, v in darker_color_result.items():
598
+ if isinstance(v, list):
599
+ darker_color_result[k] = convert_palette_for_dark_background(v)
600
+ elif isinstance(v, dict):
601
+ for k2, v2 in v.items():
602
+ darker_color_result[k][k2] = convert_palette_for_dark_background([v2])[0]
603
+ if "other" in k2.lower() and len(k2) <= 7:
604
+ darker_color_result[k][k2] = "#8f8f8f"
605
+ elif isinstance(v, str) and k != "background_color":
606
+ darker_color_result[k] = convert_palette_for_dark_background([v])[0]
607
+ elif k == "background_color":
608
+ darker_color_result[k] = random.choice(dark_background_colors)
609
+
610
+ def checkWCAG(color_result):
611
+ minWCAG = 100
612
+ for key in color_result["field"]:
613
+ minWCAG = min(minWCAG, contrast_ratio(color_result["field"][key], color_result["background_color"]))
614
+ for key in color_result["other"]:
615
+ minWCAG = min(minWCAG, contrast_ratio(color_result["other"][key], color_result["background_color"]))
616
+ return minWCAG
617
+
618
+ # 希望WCAG大于等于1.5,否则重新选背景色
619
+ best_minWCAG = checkWCAG(lighter_color_result)
620
+ # print("light WCAG", best_minWCAG)
621
+ if best_minWCAG < 1.5:
622
+ best_bcg = lighter_color_result["background_color"]
623
+ for color in random.sample(light_background_colors, len(light_background_colors)):
624
+ lighter_color_result["background_color"] = color
625
+ minWCAG = checkWCAG(lighter_color_result)
626
+ # print("now light WCAG", minWCAG)
627
+ if minWCAG > best_minWCAG:
628
+ best_bcg = color
629
+ best_minWCAG = minWCAG
630
+ if best_minWCAG >= 1.5:
631
+ break
632
+ lighter_color_result["background_color"] = best_bcg
633
+ # print("light WCAG", best_minWCAG)
634
+
635
+ # 希望WCAG大于等于3,否则重新选背景色
636
+ best_minWCAG = checkWCAG(darker_color_result)
637
+ # print("dark WCAG", best_minWCAG)
638
+ if best_minWCAG < 3:
639
+ best_bcg = darker_color_result["background_color"]
640
+ for color in random.sample(dark_background_colors, len(dark_background_colors)):
641
+ darker_color_result["background_color"] = color
642
+ minWCAG = checkWCAG(darker_color_result)
643
+ if minWCAG > best_minWCAG:
644
+ best_bcg = color
645
+ best_minWCAG = minWCAG
646
+ if best_minWCAG >= 3:
647
+ break
648
+ darker_color_result["background_color"] = best_bcg
649
+ # print("dark WCAG", best_minWCAG)
650
+
651
+ color_results2.append((lighter_color_result, darker_color_result))
652
+
653
+ def checkSeparation(color_result):
654
+ color_list = [color_result["background_color"]]
655
+ for key in color_result["field"]:
656
+ color_list.append(color_result["field"][key])
657
+ for key in color_result["other"]:
658
+ color_list.append(color_result["other"][key])
659
+ _, minD = mark_color_separation(list(set(color_list)))
660
+ return minD
661
+
662
+ random.shuffle(color_results2)
663
+
664
+ sep_thres = 20
665
+ # 过滤掉颜色区分度低(minD < sep_thres)的方案, 添加颜色方案到数据中
666
+ best_idx = 0
667
+ best_minD = checkSeparation(color_results2[best_idx][0])
668
+ # print("light minD", best_minD)
669
+ if best_minD < sep_thres:
670
+ for idx in range(1, len(color_results2)):
671
+ minD = checkSeparation(color_results2[idx][0])
672
+ if minD > best_minD:
673
+ best_minD = minD
674
+ best_idx = idx
675
+ if best_minD >= sep_thres:
676
+ break
677
+ # print("light minD", best_minD)
678
+ processed_data["colors"] = color_results2[best_idx][0]
679
+
680
+ best_idx = 0
681
+ best_minD = checkSeparation(color_results2[best_idx][1])
682
+ # print("dart minD", best_minD)
683
+ if best_minD < sep_thres:
684
+ for idx in range(1, len(color_results2)):
685
+ minD = checkSeparation(color_results2[idx][1])
686
+ if minD > best_minD:
687
+ best_minD = minD
688
+ best_idx = idx
689
+ if best_minD >= sep_thres:
690
+ break
691
+ # print("dart minD", best_minD)
692
+ processed_data["colors_dark"] = color_results2[best_idx][1]
693
+
694
+ # 保存结果
695
+ with open(output, "w", encoding="utf-8") as f:
696
+ json.dump(processed_data, f, indent=2, ensure_ascii=False)
697
+
698
+ return True
699
+
700
+ except Exception as e:
701
+ print(f"Processing {input} failed: {str(e)}")
702
+ return False
703
+
704
+ def preprocess_data(data: Dict) -> Dict:
705
+ """
706
+ 预处理数据,处理类型转换问题
707
+ """
708
+ try:
709
+ # 深拷贝避免修改原始数据
710
+ processed = data.copy()
711
+
712
+ # 确保data字段存在且格式正确
713
+ if "data" in processed and isinstance(processed["data"], dict):
714
+ # 处理数据部分
715
+ if "data" in processed["data"]:
716
+ rows = processed["data"]["data"]
717
+ if isinstance(rows, list):
718
+ # 处理每一行数据
719
+ for i, row in enumerate(rows):
720
+ if isinstance(row, dict):
721
+ # 尝试将数值字符串转换为数值类型
722
+ for key, value in row.items():
723
+ if isinstance(value, str):
724
+ try:
725
+ # 尝试转换为数值
726
+ if '.' in value:
727
+ row[key] = float(value)
728
+ else:
729
+ row[key] = int(value)
730
+ except (ValueError, TypeError):
731
+ # 如果转换失败,保持原始值
732
+ pass
733
+ elif value is None:
734
+ # 将None替换为0或其他适当的默认值
735
+ row[key] = 0
736
+
737
+ return processed
738
+
739
+ except Exception as e:
740
+ logger.error(f"数据预处理失败: {str(e)}")
741
+ raise
742
+
743
+ def main():
744
+ parser = argparse.ArgumentParser(description="Color Recommender")
745
+ parser.add_argument("--input", type=str, required=True, help="Input JSON file path")
746
+ parser.add_argument("--output", type=str, required=True, help="Output JSON file path")
747
+ parser.add_argument("--embed_model_path", type=str, default="all-MiniLM-L6-v2",
748
+ help="Path to the embedding model")
749
+ args = parser.parse_args()
750
+
751
+ success = process(input=args.input, output=args.output,
752
+ embed_model_path=args.embed_model_path)
753
+
754
+ if success:
755
+ print("Processing json successed.")
756
+ else:
757
+ print("Processing json failed.")
758
+
759
+ if __name__ == "__main__":
760
+ main()
modules/color_recommender/color_recommender_to_debug.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ 颜色推荐模块 (color_recommender)
6
+ 基于输入数据特征,自动推荐颜色
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ import argparse
12
+ from typing import Dict, List, Any, Tuple
13
+ from color_framework import ColorFramework
14
+ from color_template import ColorDesign
15
+
16
+ # 配置日志
17
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def process(input: str, output: str) -> bool:
22
+ """
23
+ 处理输入数据并生成颜色推荐
24
+
25
+ Args:
26
+ input: 输入JSON文件路径
27
+ output: 输出JSON文件路径
28
+
29
+ Returns:
30
+ 处理成功返回True,否则返回False
31
+ """
32
+ try:
33
+ # 读取输入数据
34
+ logger.info(f"读取输入文件: {input}")
35
+ with open(input, "r", encoding="utf-8") as f:
36
+ data = json.load(f)
37
+
38
+ # 抽取需要使用的数据条目
39
+ # logger.info("抽取颜色需要使用的数据条目")
40
+ assert "metadata" in data, "数据格式错误,缺少'metadata'字段"
41
+ assert "data" in data or "columns" not in data["data"], "数据格式错误,缺少'data.columns'字段"
42
+ assert "datafacts" in data, "数据格式错误,缺少'datafacts'字段"
43
+ data_info = {
44
+ "title": data["metadata"]["title"],
45
+ "description": data["metadata"]["description"],
46
+ "main_insight": data["metadata"]["main_insight"],
47
+ "columns": data["data"]["columns"],
48
+ "data_facts": data["datafacts"]
49
+ }
50
+
51
+ # 生成颜色模式推荐
52
+ # logger.info("生成颜色模式推荐")
53
+ colorframework = ColorFramework(data_info)
54
+ schemes = colorframework.load_scheme_list()
55
+ logger.info(f"获取颜色模式共{len(schemes)}种, 依次为: {schemes}")
56
+
57
+ # 默认选择第一种模式配置调色盘
58
+ # TODO: 【存在多种可选模式】选择最合适的颜色模式,或者增加随机性得到不同的颜色模式
59
+ palette = colorframework.get_infographic_palette(0)
60
+ logger.info(f"默认选择第一种模式配置调色盘: {palette}")
61
+
62
+ # 生成颜色模版,根据颜色模版配置颜色
63
+ color_design = ColorDesign(palette, lighter="high") # lighter: "high","low"
64
+ res = {}
65
+ if len(palette["main_color"]) == 1: # 仅有一个主色
66
+ res["other"] = {
67
+ "primary": color_design.main_color_hex[0],
68
+ }
69
+ elif len(palette["main_color"]) == 2: # 有两个主色
70
+ res["other"] = {
71
+ "primary": color_design.main_color_hex[0],
72
+ "secondary": color_design.main_color_hex[1],
73
+ }
74
+ else:
75
+ res["other"] = {}
76
+
77
+ use_group = False
78
+ group_col = None
79
+ x_col = None
80
+ for column in data_info["columns"]:
81
+ if column["role"] == "group":
82
+ use_group = True
83
+ group_col = column["name"]
84
+ if column["role"] == "x":
85
+ x_col = column["name"]
86
+ # TODO:【存在多种可选颜色参数】选择最合适的颜色参数,或者增加随机性得到不同的颜色模版,例如以下给出的各种seed
87
+ # get_color: seed_mark(mark颜色模式)seed_text(文本颜色模式)
88
+ # seed_color(gourp时不同颜色模式), seed_context_color(不同的上下文颜色), seed_middle_color (不同的其他中间色)
89
+
90
+ if not use_group:
91
+ unique_x_values = list(set(item[x_col] for item in data["data"]))
92
+ print("unique_x_values: ", unique_x_values)
93
+ mark_colors = color_design.get_color("marks", len(unique_x_values), group=1, seed_mark=0)
94
+ color_list = mark_colors["group1"]
95
+ # TODO: 【根据语义等其他特征调整颜色分配】目前直接assign
96
+ color_assign = {}
97
+ for i, item in enumerate(unique_x_values):
98
+ color_assign[item] = color_list[i]
99
+ res["field"] = color_assign
100
+ else:
101
+ groups = {}
102
+ group_idx = 0
103
+ group_list = []
104
+ data_group = []
105
+ for item in data["data"]:
106
+ if item[group_col] not in groups:
107
+ groups[item[group_col]] = group_idx
108
+ group_idx += 1
109
+ group_list.append(item[group_col])
110
+ data_group.append([])
111
+ data_group[groups[item[group_col]]].append(item)
112
+ # TODO: 目前要求每个group下的data数量一样,后续可以考虑不一样的情况
113
+ assert len(set([len(d) for d in data_group])) == 1, "每个group下的data数量不一样"
114
+ mark_colors = color_design.get_color("marks", len(data_group[0]), group=len(data_group), seed_mark=1)
115
+ # TODO: 【根据语义等其他特征调整颜色分配】目前根���每个group直接assign
116
+ color_assign = {}
117
+ for i, group in enumerate(data_group):
118
+ color_list = mark_colors[f"group{i+1}"]
119
+ for j, item in enumerate(group):
120
+ color_assign[item[x_col]] = color_list[j]
121
+ res["field"] = color_assign
122
+
123
+ res["background_color"] = color_design.get_color("background")[0]
124
+ text_colors = color_design.get_color("text", seed_text=1)
125
+ res["text_color"] = text_colors["annotation"][0]
126
+ res["title_color"] = text_colors["title"]
127
+ res["caption_color"] = text_colors["caption"]
128
+
129
+ used_colors = []
130
+ for key in res["field"]:
131
+ used_colors.append(res["field"][key])
132
+ used_colors.append(res["background_color"])
133
+ used_colors.append(res["text_color"])
134
+ res["available_colors"] = color_design.get_emphasis_colors(used_colors)
135
+ # TODO: 【available颜色计算】目前仅考虑了颜色距离,可以进一步筛选可用的其他颜色
136
+
137
+ # TODO: 【考虑data fact增加可以使用的颜色】
138
+ data["color"] = res
139
+
140
+ # 写入输出文件
141
+ logger.info(f"写入输出文件: {output}")
142
+ with open(output, "w", encoding="utf-8") as f:
143
+ json.dump(data, f, ensure_ascii=False, indent=2)
144
+
145
+ logger.info("颜色推荐完成")
146
+ return True
147
+
148
+ except Exception as e:
149
+ logger.error(f"处理失败: {str(e)}")
150
+ return False
151
+
152
+ if __name__ == "__main__":
153
+ parser = argparse.ArgumentParser(description="ChartPipeline - 颜色推荐模块")
154
+ parser.add_argument("--input", required=True, help="输入JSON文件路径")
155
+ parser.add_argument("--output", required=True, help="输出JSON文件路径")
156
+
157
+ args = parser.parse_args()
158
+
159
+ process(input=args.input, output=args.output)
modules/color_recommender/color_static.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import colour
2
+ import numpy as np
3
+ import random
4
+
5
+ def palette_iteration(lch_palette):
6
+ # rank the colors in the palette by hue
7
+ # linearly interpolate between colors in the palette
8
+ lchs = lch_palette[:]
9
+ lchs.sort(key=lambda x: x[2])
10
+ lchs = lchs + [lchs[0]]
11
+ res = []
12
+ for i in range(len(lchs) - 1):
13
+ lch1 = lchs[i]
14
+ lch2 = lchs[i + 1]
15
+ lch_inter = [(lch1[j] + lch2[j]) / 2 for j in range(3)]
16
+ res.append(lch1)
17
+ res.append(lch_inter)
18
+ return res
19
+
20
+ from matplotlib import colors
21
+ import matplotlib.pyplot as plt
22
+ import numpy as np
23
+
24
+ class StaticPalettes:
25
+ def __init__(self):
26
+ self.qualitative_colormaps = ['Pastel1', 'Pastel2', 'Paired', 'Accent',
27
+ 'Dark2', 'Set1', 'Set2', 'Set3', 'tab10',
28
+ 'tab20', 'tab20b', 'tab20c']
29
+
30
+ def get_colors_from_cmap(self, cmap_name, n_colors):
31
+ if cmap_name not in self.qualitative_colormaps:
32
+ raise ValueError(f"Uncertain palette name: {cmap_name}")
33
+
34
+ cmap = plt.get_cmap(cmap_name)
35
+ colors_rgb = cmap(np.linspace(0, 1, n_colors))
36
+ hex_colors = [colors.rgb2hex(rgb[:3]) for rgb in colors_rgb]
37
+ return hex_colors
38
+
39
+ def get_named_color(self, color_name):
40
+ try:
41
+ rgb = colors.to_rgb(color_name)
42
+ return colors.rgb2hex(rgb)
43
+ except ValueError:
44
+ raise ValueError(f"Uncertain color name: {color_name}")
45
+
46
+ def list_available_colormaps(self):
47
+ return self.qualitative_colormaps
48
+
49
+ def get_colors(self, n_colors):
50
+ # random select a palette
51
+ color_map = random.choice(self.qualitative_colormaps)
52
+ return self.get_colors_from_cmap(color_map, n_colors)
53
+
54
+ class EmphasisColors:
55
+ def __init__(self):
56
+ self.colors = []
57
+ # 默认添加几种强调色
58
+ self.colors.append("#FF0000")
59
+ self.colors.append("#00FF00")
60
+ self.colors.append("#0000FF")
61
+ self.colors.append("#FFFF00")
62
+ self.colors.append("#FF00FF")
63
+ self.colors.append("#00FFFF")
64
+
65
+ def add_color(self, color):
66
+ self.colors.append(color)
67
+
68
+ def get_color(self):
69
+ # 随机返回一个颜色
70
+ return random.choice(self.colors)
71
+
72
+ if __name__ == "__main__":
73
+ sp = StaticPalettes()
74
+ print(sp.get_colors_from_cmap('tab10', 10))
75
+ print(sp.get_named_color('red'))
76
+ print(sp.list_available_colormaps())
modules/color_recommender/color_template.py ADDED
@@ -0,0 +1,648 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import colour
3
+ import numpy as np
4
+ from color_static import palette_iteration, StaticPalettes
5
+
6
+ static_palettes = StaticPalettes()
7
+
8
+ def rgb_to_hex(r, g, b):
9
+ r = max(0, min(int(r), 255))
10
+ g = max(0, min(int(g), 255))
11
+ b = max(0, min(int(b), 255))
12
+ return '#{:02x}{:02x}{:02x}'.format(r, g, b)
13
+
14
+ def hex_to_rgb(hex):
15
+ hex = hex.lstrip('#')
16
+ return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4))
17
+
18
+ def random_rgb():
19
+ r = random.randint(0, 255)
20
+ g = random.randint(0, 255)
21
+ b = random.randint(0, 255)
22
+ return (r, g, b)
23
+
24
+ def random_hex():
25
+ r, g, b = random_rgb()
26
+ return rgb_to_hex(r, g, b)
27
+
28
+ def ciede2000(rgb1, rgb2):
29
+ # 转换RGB为Lab
30
+ rgb1_arr = np.array(rgb1) / 255.0
31
+ rgb2_arr = np.array(rgb2) / 255.0
32
+
33
+ lab1 = colour.XYZ_to_Lab(colour.sRGB_to_XYZ(rgb1_arr))
34
+ lab2 = colour.XYZ_to_Lab(colour.sRGB_to_XYZ(rgb2_arr))
35
+
36
+ # 计算CIEDE2000色差
37
+ delta_E = colour.delta_E(lab1, lab2, method='CIE 2000')
38
+ return delta_E
39
+
40
+ def rgb_to_hcl(r, g, b):
41
+ """
42
+ LCh (L: 0-100, C: 0-100+, h: 0-360)
43
+ """
44
+ rgb = np.array([r, g, b]) / 255.0
45
+ xyz = colour.sRGB_to_XYZ(rgb)
46
+ lab = colour.XYZ_to_Lab(xyz)
47
+ lch = colour.Lab_to_LCHab(lab)
48
+ return lch
49
+
50
+ def norm255rgb(rgb):
51
+ return [int(max(min(x*255, 255),0)) for x in rgb]
52
+
53
+ def extend_color_in_l(rgb, number=5):
54
+ lch = rgb_to_hcl(*rgb)
55
+ res = []
56
+ # for l in range(45, 95, 10):
57
+ h = lch[2]
58
+ start_l = 45 if h < 85 and h > 114 else 75
59
+ end_l = 95
60
+ delta = (end_l - start_l) / number
61
+ for i in range(number):
62
+ l = start_l + i * delta
63
+ lch[0] = l
64
+ lab = colour.LCHab_to_Lab(lch)
65
+ xyz = colour.Lab_to_XYZ(lab)
66
+ rgb = colour.XYZ_to_sRGB(xyz)
67
+ res.append(norm255rgb(rgb))
68
+ res = [rgb_to_hex(*rgb) for rgb in res]
69
+ return res
70
+
71
+ def extend_color_in_c(rgb, number=5):
72
+ lch = rgb_to_hcl(*rgb)
73
+ res = []
74
+ delta = (85 - 35) / number
75
+ for i in range(number):
76
+ c = 35 + i * delta
77
+ lch[1] = c
78
+ lab = colour.LCHab_to_Lab(lch)
79
+ xyz = colour.Lab_to_XYZ(lab)
80
+ rgb = colour.XYZ_to_sRGB(xyz)
81
+ res.append(norm255rgb(rgb))
82
+ res = [rgb_to_hex(*rgb) for rgb in res]
83
+ return res
84
+
85
+ def delta_h(h1, h2):
86
+ dh = (h1-h2) % 360
87
+ if dh < 0:
88
+ dh += 360
89
+ if dh > 180:
90
+ dh = 360 - dh
91
+ return dh
92
+
93
+ def lighter_color(rgb):
94
+ lch = rgb_to_hcl(*rgb)
95
+ if lch[0] < 80:
96
+ lch[0] += 15
97
+ if lch[1] < 70:
98
+ lch[1] += 10
99
+ lab = colour.LCHab_to_Lab(lch)
100
+ xyz = colour.Lab_to_XYZ(lab)
101
+ rgb = colour.XYZ_to_sRGB(xyz)
102
+ rgb = norm255rgb(rgb)
103
+ return rgb
104
+
105
+ def iterate_rgb_palette(rgb_palette):
106
+ lch_palette = [rgb_to_hcl(*rgb) for rgb in rgb_palette]
107
+ lch_palette = palette_iteration(lch_palette)
108
+ rgb_palette = [norm255rgb(colour.XYZ_to_sRGB(colour.Lab_to_XYZ(colour.LCHab_to_Lab(lch)))) for lch in lch_palette]
109
+ return rgb_palette
110
+
111
+ def emphasis_color(used_colors, palette):
112
+ # find the color with the largest distance to used colors in the palette
113
+ max_dist = 0
114
+ res = []
115
+ for color in palette:
116
+ min_dist = min([ciede2000(color, used_color) for used_color in used_colors])
117
+ if min_dist > 10:
118
+ res.append(rgb_to_hex(*color))
119
+ return res
120
+
121
+ def check_in_palette(color, palette):
122
+ for p in palette:
123
+ if ciede2000(color, p) < 10:
124
+ return True
125
+ return False
126
+
127
+ text_types = ['title', 'caption']
128
+ class ColorDesign:
129
+ def __init__(self, image_palette, lighter='high', seed_order = 0):
130
+ self.pool = image_palette
131
+ mode = image_palette['mode']
132
+ self.mode = mode
133
+ self.rgb_pool = [hex_to_rgb(color) for color in self.pool['main_color']]
134
+ if lighter == 'high':
135
+ self.rgb_pool_cp = [lighter_color(rgb) for rgb in self.rgb_pool]
136
+ self.rgb_pool = self.rgb_pool_cp
137
+ seed_order = seed_order % len(self.rgb_pool)
138
+ self.rgb_pool = self.rgb_pool[seed_order:] + self.rgb_pool[:seed_order]
139
+ self.rgb_pool_hex = [rgb_to_hex(*rgb) for rgb in self.rgb_pool]
140
+ self.main_color_hex = self.rgb_pool_hex
141
+ self.middle_color = None
142
+
143
+ black = (0, 0, 0)
144
+ white = (255, 255, 255)
145
+ gray1 = (75, 75, 75)
146
+ gray2 = (150, 150, 150)
147
+ gray3 = (200, 200, 200)
148
+ self.rgb_pool.sort(key=lambda x: ciede2000(x, black))
149
+
150
+ bcg = image_palette['bcg']
151
+ bcg_rgb = hex_to_rgb(bcg)
152
+ self.main_color = self.rgb_pool
153
+ self.bcg_color = bcg_rgb
154
+ dist_color_2_black = ciede2000(self.bcg_color, black)
155
+ dist_color_2_white = ciede2000(self.bcg_color, white)
156
+ if dist_color_2_black > dist_color_2_white:
157
+ self.lightness = 'light'
158
+ else:
159
+ self.lightness = 'dark'
160
+
161
+ if mode == 'colorful':
162
+ iter_rgb2 = iterate_rgb_palette(self.rgb_pool)
163
+ iter_rgb3 = iterate_rgb_palette(iter_rgb2)
164
+ self.rgb_pool_hex_2 = [rgb_to_hex(*rgb) for rgb in iter_rgb2]
165
+ self.rgb_pool_hex_3 = [rgb_to_hex(*rgb) for rgb in iter_rgb3]
166
+
167
+ self.basic_colors = [black, white, gray1, gray2, gray3]
168
+ self.basic_colors_hex = [rgb_to_hex(*color) for color in self.basic_colors]
169
+
170
+
171
+ def get_emphasis_colors(self, used_colors):
172
+ if isinstance(used_colors[0], str):
173
+ used_colors = [hex_to_rgb(color) for color in used_colors]
174
+ hex_pool = self.pool['other_colors'] + self.pool['main_color']
175
+ if self.mode == 'colorful':
176
+ hex_pool += self.rgb_pool_hex_2 + self.rgb_pool_hex_3
177
+ palette = [hex_to_rgb(color) for color in hex_pool]
178
+ used_colors.append(self.bcg_color)
179
+ return emphasis_color(used_colors, palette)
180
+
181
+ def get_reverse_color(self, color, seed = 0):
182
+ bcg_color_hex = rgb_to_hex(*self.bcg_color)
183
+ if color == bcg_color_hex:
184
+ return self.get_color('text', seed_text = seed, reverse = True)['title'][0]
185
+ else:
186
+ if self.lightness == 'dark':
187
+ if seed % 2 == 0:
188
+ return self.basic_colors_hex[1]
189
+ return bcg_color_hex
190
+ else:
191
+ if seed % 2 == 0:
192
+ return self.basic_colors_hex[0]
193
+ return bcg_color_hex
194
+
195
+ def get_color(self, type, number = 1, group = 1, \
196
+ seed_color = 0, seed_context_color = 0, seed_middle_color = 0, \
197
+ seed_text = 0, seed_mark = 0, seed_axis = 0):
198
+ if type == 'background':
199
+ return [self.pool['bcg']]
200
+
201
+ if self.mode == 'monochrome':
202
+ main_color = self.main_color[0]
203
+ if group == 1:
204
+ if number < 6:
205
+ extend_colors1 = extend_color_in_l(main_color)
206
+ extend_colors2 = extend_color_in_c(main_color)
207
+ else:
208
+ extend_colors1 = extend_color_in_l(main_color, number)
209
+ extend_colors2 = extend_color_in_c(main_color, number)
210
+ else:
211
+ if group < 6:
212
+ extend_colors1 = extend_color_in_l(main_color)
213
+ extend_colors2 = extend_color_in_c(main_color)
214
+ else:
215
+ extend_colors1 = extend_color_in_l(main_color, group)
216
+ extend_colors2 = extend_color_in_c(main_color, group)
217
+
218
+ main_color_hex = rgb_to_hex(*main_color)
219
+ if self.lightness == 'dark':
220
+ other_color = self.basic_colors_hex[1]
221
+ gray_color = self.basic_colors_hex[4]
222
+ else:
223
+ other_color = self.basic_colors_hex[0]
224
+ gray_color = self.basic_colors_hex[2]
225
+ if seed_context_color > 0 and len(self.pool['context_color']) > 0:
226
+ choice = seed_context_color % len(self.pool['context_color'])
227
+ gray_color = self.pool['context_color'][choice]
228
+
229
+ if type == 'text':
230
+ seed = seed_text % 4
231
+ if seed == 0: # all same color
232
+ return {
233
+ 'title': [main_color_hex],
234
+ 'caption': [main_color_hex],
235
+ 'annotation': [main_color_hex],
236
+ }
237
+ if seed == 1: # all black/white
238
+ return {
239
+ 'title': [other_color],
240
+ 'caption': [other_color],
241
+ 'annotation': [other_color],
242
+ }
243
+ if seed == 2: # extend in lightness
244
+ if self.lightness == 'dark':
245
+ res = extend_colors1[-3:]
246
+ else:
247
+ res = extend_colors1[:3]
248
+ return {
249
+ 'title': res[:1],
250
+ 'caption': res[1:2],
251
+ 'annotation': res[2:3],
252
+ }
253
+ if seed == 3: # main color + other black/white
254
+ return {
255
+ 'title': [main_color_hex],
256
+ 'caption': [other_color],
257
+ 'annotation': [other_color],
258
+ }
259
+
260
+ if type == 'marks':
261
+ if group == 1:
262
+ seed = seed_mark % 6
263
+ if seed == 0:
264
+ return {
265
+ 'group1': [main_color_hex for _ in range(number)],
266
+ }
267
+ if seed == 1:
268
+ return {
269
+ 'group1': extend_colors1[:number],
270
+ }
271
+ if seed == 2:
272
+ return {
273
+ 'group1': extend_colors1[-number:],
274
+ }
275
+ if seed == 3:
276
+ return {
277
+ 'group1': extend_colors2[:number],
278
+ }
279
+ if seed == 4:
280
+ return {
281
+ 'group1': extend_colors2[-number:],
282
+ }
283
+ if seed == 5:
284
+ return {
285
+ 'group1': [gray_color for _ in range(number)],
286
+ }
287
+ else:
288
+ seed = seed_mark % 4
289
+ group_colors = []
290
+ if seed == 0:
291
+ group_colors = extend_colors1[:group]
292
+ if seed == 1:
293
+ group_colors = extend_colors1[-group:]
294
+ if seed == 2:
295
+ group_colors = extend_colors2[:group]
296
+ if seed == 3:
297
+ group_colors = extend_colors2[-group:]
298
+ res = {}
299
+ for i in range(group):
300
+ res[f'group{i+1}'] = group_colors
301
+ return res
302
+
303
+ if type == 'axis':
304
+ seed = seed_axis % 5
305
+ if seed == 0:
306
+ return {
307
+ 'axis': [extend_colors1[0]]
308
+ }
309
+ if seed == 1:
310
+ return {
311
+ 'axis': [extend_colors2[0]]
312
+ }
313
+ if seed == 3: # gray
314
+ return {
315
+ 'axis': [gray_color]
316
+ }
317
+ if seed == 4: # main color
318
+ return {
319
+ 'axis': [main_color_hex]
320
+ }
321
+ return {
322
+ 'axis': [other_color]
323
+ }
324
+
325
+ if self.mode == 'dual-color':
326
+ selected_color = self.main_color
327
+ color1 = selected_color[0]
328
+ color2 = selected_color[1]
329
+
330
+ if number < 6:
331
+ extend_colors1_l = extend_color_in_l(color1)
332
+ extend_colors2_l = extend_color_in_l(color2)
333
+ extend_colors1_c = extend_color_in_c(color1)
334
+ extend_colors2_c = extend_color_in_c(color2)
335
+ else:
336
+ extend_colors1_l = extend_color_in_l(color1, number)
337
+ extend_colors2_l = extend_color_in_l(color2, number)
338
+ extend_colors1_c = extend_color_in_c(color1, number)
339
+ extend_colors2_c = extend_color_in_c(color2, number)
340
+
341
+ color1_hex = rgb_to_hex(*color1)
342
+ color2_hex = rgb_to_hex(*color2)
343
+ main_color1_hex = color1_hex
344
+ main_color2_hex = color2_hex
345
+
346
+ middle_colors = [color for color in self.pool['other_colors']]
347
+ # print("middle_colors: ", middle_colors)
348
+ if len(middle_colors) == 0:
349
+ middle_colors = ["#ababab"]
350
+ middle_color_seed = seed_middle_color % len(middle_colors)
351
+ middle_color = middle_colors[middle_color_seed]
352
+
353
+ other_color = None
354
+ gray_color = None
355
+ if self.lightness == 'dark':
356
+ other_color = self.basic_colors_hex[1]
357
+ gray_color = self.basic_colors_hex[4]
358
+ else:
359
+ other_color = self.basic_colors_hex[0]
360
+ gray_color = self.basic_colors_hex[2]
361
+ if seed_context_color > 0 and len(self.pool['context_color']) > 0:
362
+ choice = seed_context_color % len(self.pool['context_color'])
363
+ gray_color = self.pool['context_color'][choice]
364
+
365
+ if type == 'text':
366
+ seed = seed_text % 14
367
+ if seed == 0:
368
+ return {
369
+ 'title': [other_color],
370
+ 'caption': [other_color],
371
+ 'annotation': [other_color],
372
+ }
373
+ if seed == 1:
374
+ return {
375
+ 'title': [other_color],
376
+ 'caption': [gray_color],
377
+ 'annotation': [gray_color],
378
+ }
379
+ if seed == 2:
380
+ return {
381
+ 'title': [main_color1_hex],
382
+ 'caption': [other_color],
383
+ 'annotation': [other_color],
384
+ }
385
+ if seed == 3:
386
+ return {
387
+ 'title': [main_color2_hex],
388
+ 'caption': [other_color],
389
+ 'annotation': [other_color],
390
+ }
391
+ if seed == 4:
392
+ return {
393
+ 'title': [main_color1_hex],
394
+ 'caption': [main_color1_hex, main_color2_hex],
395
+ 'annotation': [other_color],
396
+ }
397
+ if seed == 5:
398
+ return {
399
+ 'title': [main_color2_hex],
400
+ 'caption': [main_color1_hex, main_color2_hex],
401
+ 'annotation': [other_color],
402
+ }
403
+ if seed == 6:
404
+ return {
405
+ 'title': [other_color],
406
+ 'caption': [main_color1_hex, main_color2_hex],
407
+ 'annotation': [other_color],
408
+ }
409
+ if seed == 7:
410
+ return {
411
+ 'title': [main_color1_hex],
412
+ 'caption': [main_color1_hex, main_color2_hex],
413
+ 'annotation': [gray_color],
414
+ }
415
+ if seed == 8:
416
+ return {
417
+ 'title': [main_color2_hex],
418
+ 'caption': [main_color1_hex, main_color2_hex],
419
+ 'annotation': [gray_color],
420
+ }
421
+ if seed == 9:
422
+ return {
423
+ 'title': [other_color],
424
+ 'caption': [main_color1_hex, main_color2_hex],
425
+ 'annotation': [gray_color],
426
+ }
427
+ if seed == 10:
428
+ return {
429
+ 'title': [main_color1_hex],
430
+ 'caption': [other_color, main_color1_hex, main_color2_hex],
431
+ 'annotation': [gray_color],
432
+ }
433
+ if seed == 11:
434
+ return {
435
+ 'title': [main_color2_hex],
436
+ 'caption': [other_color, main_color1_hex, main_color2_hex],
437
+ 'annotation': [gray_color],
438
+ }
439
+ if seed == 12:
440
+ return {
441
+ 'title': [other_color],
442
+ 'caption': [other_color, main_color1_hex, main_color2_hex],
443
+ 'annotation': [gray_color],
444
+ }
445
+ if seed == 13:
446
+ return {
447
+ 'title': [main_color1_hex, main_color2_hex, middle_color],
448
+ 'caption': [other_color],
449
+ 'annotation': [gray_color],
450
+ }
451
+
452
+ if type == 'marks':
453
+ if group == 1 and number == 2:
454
+ return {
455
+ 'group1': [main_color1_hex, main_color2_hex],
456
+ }
457
+ if group == 1:
458
+ seed = seed_mark % 4
459
+ if seed == 0:
460
+ return {
461
+ 'group1': [other_color],
462
+ }
463
+ if seed == 1:
464
+ return {
465
+ 'group1': [main_color1_hex],
466
+ }
467
+ if seed == 2:
468
+ return {
469
+ 'group1': [main_color2_hex],
470
+ }
471
+ if seed == 3:
472
+ return {
473
+ 'group1': [gray_color],
474
+ }
475
+ else:
476
+ assert group == 2
477
+ seed = seed_mark % 5
478
+ if seed == 0:
479
+ return {
480
+ 'group1': [main_color1_hex],
481
+ 'group2': [main_color2_hex],
482
+ }
483
+ if seed == 1:
484
+ return {
485
+ 'group1': extend_colors1_l[:number],
486
+ 'group2': extend_colors2_l[:number],
487
+ }
488
+ if seed == 2:
489
+ return {
490
+ 'group1': extend_colors1_l[-number:],
491
+ 'group2': extend_colors2_l[-number:],
492
+ }
493
+ if seed == 3:
494
+ return {
495
+ 'group1': extend_colors1_c[:number],
496
+ 'group2': extend_colors2_c[:number],
497
+ }
498
+ if seed == 4:
499
+ return {
500
+ 'group1': extend_colors1_c[-number:],
501
+ 'group2': extend_colors2_c[-number:],
502
+ }
503
+
504
+ if type == 'axis':
505
+ axis_seed = seed_axis % 5
506
+ if axis_seed == 0:
507
+ return {
508
+ 'axis': [other_color],
509
+ }
510
+ if axis_seed == 1:
511
+ return {
512
+ 'axis': [gray_color],
513
+ }
514
+ if axis_seed == 2:
515
+ return {
516
+ 'axis': [extend_colors1_l[0]],
517
+ }
518
+ if axis_seed == 3:
519
+ return {
520
+ 'axis': [extend_colors2_l[0]],
521
+ }
522
+ if axis_seed == 4:
523
+ return {
524
+ 'axis': [middle_color],
525
+ }
526
+
527
+ if self.mode == 'colorful':
528
+ other_color = None
529
+ gray_color = None
530
+ if self.lightness == 'dark':
531
+ other_color = self.basic_colors_hex[1]
532
+ gray_color = self.basic_colors_hex[4]
533
+ else:
534
+ other_color = self.basic_colors_hex[0]
535
+ gray_color = self.basic_colors_hex[2]
536
+ if seed_context_color > 0 and len(self.pool['context_color']) > 0:
537
+ choice = seed_context_color % len(self.pool['context_color'])
538
+ gray_color = self.pool['context_color'][choice]
539
+
540
+ if type == 'text':
541
+ seed = seed_text % 2
542
+ if seed == 0:
543
+ return {
544
+ 'title': [other_color],
545
+ 'caption': [other_color],
546
+ 'annotation': [other_color],
547
+ }
548
+ if self.middle_color is None:
549
+ rand_color = random.choice(self.rgb_pool_hex)
550
+ extend_colors = extend_color_in_l(hex_to_rgb(rand_color))
551
+ dark_rand_color = extend_colors[0] if self.lightness == 'dark' else extend_colors[-1]
552
+ self.middle_color = {
553
+ 'color': rand_color,
554
+ 'dark': dark_rand_color,
555
+ }
556
+ return {
557
+ 'title': [self.middle_color['dark']],
558
+ 'caption': [self.middle_color['dark']],
559
+ 'annotation': [self.middle_color['dark']]
560
+ }
561
+ if type == 'marks':
562
+ seed = seed_mark % 2 + 1
563
+ if group == 1:
564
+ clist = []
565
+ if len(self.rgb_pool_hex) >= number:
566
+ if seed == 1:
567
+ clist = self.rgb_pool_hex[:number]
568
+ if seed == 2:
569
+ clist = self.rgb_pool_hex[-number:]
570
+ elif len(self.rgb_pool_hex_2) >= number:
571
+ if seed == 1:
572
+ clist = self.rgb_pool_hex_2[:number]
573
+ if seed == 2:
574
+ clist = self.rgb_pool_hex_2[-number:]
575
+ elif len(self.rgb_pool_hex_3) >= number:
576
+ if seed == 1:
577
+ clist = self.rgb_pool_hex_3[:number]
578
+ if seed == 2:
579
+ clist = self.rgb_pool_hex_3[-number:]
580
+ else:
581
+ clist = static_palettes.get_colors(number)
582
+ return {
583
+ 'group1': clist,
584
+ }
585
+ else:
586
+ base_colors = []
587
+ if len(self.rgb_pool_hex) >= group:
588
+ if seed == 1:
589
+ base_colors = self.rgb_pool_hex[:group]
590
+ if seed == 2:
591
+ base_colors = self.rgb_pool_hex[-group:]
592
+ elif len(self.rgb_pool_hex_2) >= group:
593
+ if seed == 1:
594
+ base_colors = self.rgb_pool_hex_2[:group]
595
+ if seed == 2:
596
+ base_colors = self.rgb_pool_hex_2[-group:]
597
+ elif len(self.rgb_pool_hex_3) >= group:
598
+ if seed == 1:
599
+ base_colors = self.rgb_pool_hex_3[:group]
600
+ if seed == 2:
601
+ base_colors = self.rgb_pool_hex_3[-group:]
602
+ else:
603
+ base_colors = static_palettes.get_colors(group)
604
+ if seed_color % 2 == 0:
605
+ res = {}
606
+ for i in range(group):
607
+ res[f'group{i+1}'] = [base_colors[i] for _ in range(number)]
608
+ else:
609
+ res = {}
610
+ for i in range(group):
611
+ res[f'group{i+1}'] = extend_color_in_l(hex_to_rgb(base_colors[i]), number)
612
+ return res
613
+
614
+ if type == 'axis':
615
+ seed = seed_axis % 3
616
+ if seed == 0:
617
+ return {
618
+ 'axis': [gray_color],
619
+ }
620
+ if seed == 1:
621
+ return {
622
+ 'axis': [other_color],
623
+ }
624
+ return {
625
+ 'axis': [random.choice(self.rgb_pool_hex)]
626
+ }
627
+
628
+ def rank_color(self, palette, importance):
629
+ assert len(palette) == len(importance)
630
+ dists_to_bcg = [ciede2000(hex_to_rgb(color), self.bcg_color) for color in palette]
631
+ # sort by importance, high importance with high dist to bcg
632
+ # low importance with low dist to bcg
633
+ dist_sorted_indices = sorted(range(len(dists_to_bcg)), key=lambda i: dists_to_bcg[i], reverse=True)
634
+ imp_sorted_indices = sorted(range(len(importance)), key=lambda i: importance[i], reverse=True)
635
+
636
+ ranked_palette = [None] * len(palette)
637
+
638
+ for rank, color_idx in enumerate(dist_sorted_indices):
639
+ ranked_palette[imp_sorted_indices[rank]] = palette[color_idx]
640
+
641
+ return ranked_palette
642
+
643
+ def rank_color_by_contrast(self, palette):
644
+ dists_to_bcg = [ciede2000(hex_to_rgb(color), self.bcg_color) for color in palette]
645
+ print("bcg_color: ", self.bcg_color)
646
+ sorted_colors = sorted(zip(palette, dists_to_bcg), key=lambda x: -x[1])
647
+ ranked_palette = [color for color, _ in sorted_colors]
648
+ return ranked_palette
modules/color_recommender/create_index.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import os
4
+ from modules.color_recommender.color_index_builder import ColorIndexBuilder
5
+
6
+ def main(force: bool=False):
7
+ parser = argparse.ArgumentParser(description='Build color palette index using FAISS and SentenceTransformer')
8
+ parser.add_argument('--input', '-i', type=str, default='./static/color_palette.json',
9
+ help='Path to input color palette JSON file')
10
+ parser.add_argument('--output', '-o', type=str, default='./static/color_palette.index',
11
+ help='Path to save the FAISS index')
12
+ parser.add_argument('--force', '-f', action='store_true',
13
+ help='Force rebuild even if index exists')
14
+ parser.add_argument('--embed_model_path', type=str, default='', help='Path to sentence embedding model (optional)')
15
+
16
+
17
+ args = parser.parse_args()
18
+
19
+ # Check if input file exists
20
+ if not os.path.exists(args.input):
21
+ print(f"Error: Input file {args.input} does not exist")
22
+ return 1
23
+
24
+ # Check if output file exists and handle force flag
25
+ if os.path.exists(args.output) and not (force or args.force):
26
+ print(f"Index file {args.output} already exists. Use --force to rebuild.")
27
+ return 0
28
+
29
+ try:
30
+ # Initialize and build index
31
+ print("Initializing ColorIndexBuilder...")
32
+ index_builder = ColorIndexBuilder(args.input, args.embed_model_path)
33
+
34
+ print("Building index...")
35
+ index_builder.build_index()
36
+
37
+ print(f"Saving index to {args.output}...")
38
+ index_builder.save_index(args.output)
39
+
40
+ print("Index built successfully!")
41
+ return 0
42
+
43
+ except Exception as e:
44
+ print(f"Error building index: {str(e)}")
45
+ return 1
46
+
47
+ if __name__ == '__main__':
48
+ exit(main())
modules/color_recommender/infographic_retrieve.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json, os
2
+ import torch
3
+ import numpy as np
4
+ from sentence_transformers import SentenceTransformer, util
5
+ from config import sentence_transformer_path, infographic_library_path, infographic_image_path
6
+ model_path = sentence_transformer_path
7
+ library_path = infographic_library_path
8
+ image_root_path = infographic_image_path
9
+
10
+ class InfographicRetriever:
11
+ def __init__(self, model_path, library_path, image_path):
12
+ """
13
+ Initialize the image retriever with a knowledge base and embedding model
14
+
15
+ Args:
16
+ library_path (str): Path to the JSON knowledge base file
17
+ model_path (str): Path to the sentence transformer model
18
+ """
19
+ self.model_path = model_path
20
+ self.library_path = library_path
21
+ self.image_path = image_path
22
+
23
+ # Load knowledge base
24
+ with open(library_path, 'r', encoding='utf-8') as f:
25
+ self.knowledge_base = json.load(f)
26
+
27
+ # Load embedding model
28
+ self.embedding_model = SentenceTransformer(model_path)
29
+
30
+ # Pre-compute embeddings
31
+ self._prepare_embeddings()
32
+
33
+ def _combine_text_fields(self, item):
34
+ """Combine different text fields into a single string"""
35
+ return f"{item['title']} {item['description']} {item['main_insight']}" + " ".join(item['columns'])
36
+
37
+ def _prepare_embeddings(self):
38
+ """Pre-compute embeddings for all items in knowledge base"""
39
+ self.knowledge_texts = []
40
+ self.knowledge_ids = []
41
+
42
+ for key, record in self.knowledge_base.items():
43
+ combined_text = self._combine_text_fields(record)
44
+ self.knowledge_texts.append(combined_text)
45
+ self.knowledge_ids.append(key)
46
+
47
+ with torch.no_grad():
48
+ self.knowledge_embeddings = self.embedding_model.encode(
49
+ self.knowledge_texts,
50
+ convert_to_tensor=True,
51
+ normalize_embeddings=True
52
+ )
53
+
54
+ def retrieve_similar_entries(self, query_text, top_k=3):
55
+ """
56
+ Retrieve similar images based on text query
57
+
58
+ Args:
59
+ query_text (str): Text query to search for
60
+ top_k (int): Number of results to return
61
+
62
+ Returns:
63
+ list: List of dictionaries containing similar entries
64
+ """
65
+ with torch.no_grad():
66
+ query_emb = self.embedding_model.encode(
67
+ query_text,
68
+ convert_to_tensor=True,
69
+ normalize_embeddings=True
70
+ )
71
+
72
+ cosine_scores = util.cos_sim(query_emb, self.knowledge_embeddings)[0]
73
+ top_results = torch.topk(cosine_scores, k=top_k)
74
+
75
+ results = []
76
+ for score_idx, score_val in zip(top_results.indices, top_results.values):
77
+ idx = score_idx.item()
78
+ similarity = score_val.item()
79
+ doc_id = self.knowledge_ids[idx]
80
+ results.append((os.path.join(self.image_path, doc_id + '.jpeg'), similarity, doc_id))
81
+ return results
82
+
83
+ if __name__ == "__main__":
84
+ retriever = InfographicRetriever(model_path, library_path, image_root_path)
85
+
86
+ user_query = "Cat in a hat"
87
+ similar_entries = retriever.retrieve_similar_entries(user_query, top_k=10)
88
+ from IPython import embed; embed()
modules/color_recommender/llm_api.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+ from PIL import Image
3
+ import base64
4
+ from io import BytesIO
5
+ from config import client_key, base_url
6
+
7
+ image_max_size = 512
8
+ model_name = 'gpt-4o-mini'
9
+
10
+ client = OpenAI(
11
+ api_key=client_key,
12
+ base_url=base_url
13
+ )
14
+
15
+ def resize_image(img, max_size=512):
16
+ width, height = img.size
17
+ ratio = min(max_size / width, max_size / height)
18
+ if ratio >= 1:
19
+ return img
20
+ new_width = int(width * ratio)
21
+ new_height = int(height * ratio)
22
+ resized_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
23
+ return resized_img
24
+
25
+ def image_to_base64(image_path, target_size=image_max_size):
26
+ with Image.open(image_path) as img:
27
+ # img = img.resize(size)
28
+ img = resize_image(img, target_size)
29
+ buffered = BytesIO()
30
+ img.save(buffered, format="PNG")
31
+ img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
32
+ return img_base64
33
+
34
+ wwxxhh = 0
35
+ def ask(prompt):
36
+ global wwxxhh
37
+ number_of_trials = 0
38
+ while number_of_trials < 5:
39
+ try:
40
+ response = client.chat.completions.create(
41
+ model=model_name,
42
+ messages=[
43
+ {
44
+ "role": "user",
45
+ "content": [
46
+ {
47
+ "type": "text",
48
+ "text": prompt},
49
+ ],
50
+ }
51
+ ]
52
+ )
53
+ wwxxhh += response.usage.total_tokens
54
+ return response.choices[0].message.content
55
+
56
+ except Exception as e:
57
+ number_of_trials += 1
58
+ print(e)
59
+
60
+ return 'Error!'
61
+
62
+ def ask_image(prompt, image_data):
63
+ global wwxxhh
64
+ number_of_trials = 0
65
+ while number_of_trials < 5:
66
+ try:
67
+ response = client.chat.completions.create(
68
+ model=model_name,
69
+ messages=[
70
+ {
71
+ "role": "user",
72
+ "content": [
73
+ {
74
+ "type": "text",
75
+ "text": prompt},
76
+ {
77
+ "type": "image_url",
78
+ "image_url": {
79
+ "url": f"data:image/jpeg;base64,{image_data}"
80
+ },
81
+ },
82
+ ],
83
+ }
84
+ ]
85
+ )
86
+ wwxxhh += response.usage.total_tokens
87
+ return response.choices[0].message.content
88
+
89
+ except Exception as e:
90
+ number_of_trials += 1
91
+ print(e)
92
+
93
+ return 'Error!'
94
+
95
+ def chat_with_image(prompts, image_data):
96
+ global wwxxhh
97
+ messages = []
98
+
99
+ number_of_trials = 0
100
+ while number_of_trials < 5:
101
+ try:
102
+ messages.append({
103
+ "role": "user",
104
+ "content": [
105
+ {
106
+ "type": "text",
107
+ "text": prompts[0]
108
+ },
109
+ {
110
+ "type": "image_url",
111
+ "image_url": {
112
+ "url": f"data:image/jpeg;base64,{image_data}"
113
+ }
114
+ }
115
+ ]
116
+ })
117
+
118
+ response = client.chat.completions.create(
119
+ model=model_name,
120
+ messages=messages
121
+ )
122
+
123
+ messages.append({
124
+ "role": "assistant",
125
+ "content": response.choices[0].message.content
126
+ })
127
+
128
+ wwxxhh += response.usage.total_tokens
129
+
130
+ for prompt in prompts[1:]:
131
+ messages.append({
132
+ "role": "user",
133
+ "content": [{"type": "text", "text": prompt}]
134
+ })
135
+
136
+ response = client.chat.completions.create(
137
+ model=model_name,
138
+ messages=messages
139
+ )
140
+
141
+ messages.append({
142
+ "role": "assistant",
143
+ "content": response.choices[0].message.content
144
+ })
145
+
146
+ wwxxhh += response.usage.total_tokens
147
+
148
+ return [msg["content"] for msg in messages if msg["role"] == "assistant"]
149
+
150
+ except Exception as e:
151
+ number_of_trials += 1
152
+ print(e)
153
+
154
+ return ['Error!'] * len(prompts)