feat: color-palette-generator - agentic image color extraction with K…#34
feat: color-palette-generator - agentic image color extraction with K…#34A-VISHAL wants to merge 21 commits into
Conversation
…rk/Oxtools into screenshot_accuracy
…Means + LLM refinement
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds a new tier-2 “Color Palette Generator” tool that extracts dominant image colors (KMeans) and optionally refines them via an Oxlo-backed LLM pipeline, plus frontend updates to support image upload, long-running execution, and richer palette/image previews.
Changes:
- Introduces a new Python tool (
image-palette-extractor) with KMeans extraction, LangGraph orchestration, and LLM refinement. - Updates the frontend tool definition and tool execution hook for tier-2 image workflows (upload, compression, longer timeouts).
- Adds/updates result rendering to show early extracted colors and an interactive image hover/pixel-inspection viewer.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| services/python-tools/tools/image-palette-extractor/tool.py | Tool entrypoint + streaming output markers + fallback behavior |
| services/python-tools/tools/image-palette-extractor/requirements.txt | Tool-specific Python dependencies |
| services/python-tools/tools/image-palette-extractor/pipeline.py | LangGraph state machine for validate→extract→refine→format |
| services/python-tools/tools/image-palette-extractor/llm_refiner.py | LLM call + JSON extraction + WCAG “compliance” wrapper |
| services/python-tools/tools/image-palette-extractor/config.py | Oxlo ChatOpenAI client config + refinement system prompt |
| services/python-tools/tools/image-palette-extractor/color_extractor.py | Base64 image decoding + KMeans clustering + preview/pixel-map helper |
| app/src/lib/tools/color-palette.ts | Switches tool to tier2 + image input + model metadata |
| app/src/hooks/use-tool-execution.ts | Adds per-tool timeouts and abort handling |
| app/src/components/result-viewer.tsx | Parses new output markers + renders early extracted colors + uses new viewer |
| app/src/components/color-palette-viewer.tsx | New interactive image/palette viewer (canvas hover inspection) |
| app/src/app/tools/[toolId]/page.tsx | Client-side image validation/compression + tier2 execution wiring |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Re-encode resized image to data URI (jpeg to reduce size) | ||
| buffer = io.BytesIO() | ||
| image.save(buffer, format="JPEG", quality=85) | ||
| buffer.seek(0) | ||
| resized_b64 = buffer.getvalue() | ||
| from base64 import b64encode | ||
|
|
||
| data_uri = f"{header};base64,{b64encode(resized_b64).decode('utf-8')}" | ||
|
|
| image_array = np.array(image) | ||
|
|
||
| # Determine sampling step to keep pixel count <= max_pixels | ||
| total = width * height | ||
| pixels = [] | ||
| if max_pixels and max_pixels > 0: | ||
| if total <= max_pixels: | ||
| step = 1 | ||
| else: | ||
| # sample roughly uniformly using a square step | ||
| step = int(max(1, (total / max_pixels) ** 0.5)) | ||
|
|
||
| for y in range(0, height, step): | ||
| for x in range(0, width, step): | ||
| rgb = image_array[y, x] | ||
| hex_color = self._rgb_to_hex(tuple(rgb)) | ||
| pixels.append({"x": int(x), "y": int(y), "color": hex_color}) | ||
| else: | ||
| step = 0 |
| "roles": { | ||
| "primary": extracted_colors[0] if extracted_colors else "#000000", | ||
| "secondary": extracted_colors[1] if len(extracted_colors) > 1 else extracted_colors[0], | ||
| "accent": extracted_colors[2] if len(extracted_colors) > 2 else extracted_colors[0], | ||
| "background": "#ffffff", | ||
| "surface": "#f5f5f5", | ||
| "text": "#333333", | ||
| "muted": "#999999", |
| # CREATE FALLBACK PALETTE from extracted colors | ||
| result = { | ||
| "success": True, | ||
| "palette": {color: color for color in extracted_colors[:min(num_colors, len(extracted_colors))]}, | ||
| "roles": { | ||
| "primary": extracted_colors[0] if extracted_colors else "#000000", | ||
| "secondary": extracted_colors[1] if len(extracted_colors) > 1 else extracted_colors[0], | ||
| "accent": extracted_colors[2] if len(extracted_colors) > 2 else extracted_colors[0], | ||
| "background": "#ffffff", | ||
| "surface": "#f5f5f5", | ||
| "text": "#333333", | ||
| "muted": "#999999", | ||
| }, |
| def _load_module_from_file(filename: str): | ||
| """Load a module explicitly from a file path to avoid collisions.""" | ||
| module_name = filename.replace('.py', '') | ||
|
|
||
| # Clear from sys.modules cache to force fresh load | ||
| if module_name in sys.modules: | ||
| del sys.modules[module_name] | ||
|
|
There was a problem hiding this comment.
Please look into this is as well, it can cause issues further, so have config.py file for this and have them imported.
| if (err instanceof DOMException && err.name === "AbortError") { | ||
| // Request was aborted (either by timeout or user action) | ||
| if (!error) { // Only set timeout error if not already set | ||
| setError({ | ||
| message: "Request was cancelled. Please try again.", | ||
| code: "aborted" | ||
| }); | ||
| } | ||
| setResult(""); | ||
| return; |
| @@ -71,6 +105,7 @@ function ToolPageContent({ toolId }: { toolId: string }) { | |||
| const { canExecute, getToolUsage, trackExecution, redirectToUpgrade } = useAuth(); | |||
| const toolUsage = getToolUsage(tool.id); | |||
| const [showUpgradeDialog, setShowUpgradeDialog] = useState(false); | |||
| const [hasExecuted, setHasExecuted] = useState(false); | |||
Resolved conflicts in result-viewer.tsx by accepting HEAD version which includes: - Color palette extraction and display features - Pipeline logs viewer - Extracted colors preview with copy functionality Note: Incoming branch's Edit/Compare tabs and fullscreen editing features should be integrated in a future PR for full feature parity.
| def _load_module_from_file(filename: str): | ||
| """Load a module explicitly from a file path to avoid collisions.""" | ||
| module_name = filename.replace('.py', '') | ||
|
|
||
| # Clear from sys.modules cache to force fresh load | ||
| if module_name in sys.modules: | ||
| del sys.modules[module_name] | ||
|
|
There was a problem hiding this comment.
Please look into this is as well, it can cause issues further, so have config.py file for this and have them imported.
There was a problem hiding this comment.
There is still some extension related codes, please remove the extension related codes and files.
| "chrome-extension://poaainbnlkonlkjiiemhfoflbkobamec" | ||
| ] | ||
| }, | ||
| allowedDevOrigins: ["chrome-extension://poaainbnlkonlkjiiemhfoflbkobamec"], |
There was a problem hiding this comment.
Extension related codes.
| /// <reference types="next" /> | ||
| /// <reference types="next/image-types/global" /> | ||
| import "./.next/dev/types/routes.d.ts"; | ||
| import "./.next/types/routes.d.ts"; |
There was a problem hiding this comment.
Can you check on this? Why this was edited. Because this files were mentioned not to be edited. So once can you please look into this why this was edited.
…Means + LLM refinement
Oxtools Submission
Project name: [Your project name]
Contributor: [@your-github-handle]
Demo: [Link to Loom or YouTube recording — required]
What does this tool do?
Write 2–3 sentences describing the tool, the problem it solves, and how it uses the Oxlo API.
Submission checklist
Check every box before requesting a review. Unchecked items will result in the PR being sent back.
Structure
projects/[my-project-name]/Required files
Dockerfileis present anddocker build .succeedsdocker-compose.ymlis present anddocker compose upstarts the appoxlo-manifest.jsonis present and all fields are filled in.env.examplelists every environment variable the project needs (with empty values)README.mdis present with setup instructions a reviewer can follow exactlySecurity
.envfile is not included in this PRgit grep -i "api_key"and found no leaksOxlo API
OXLO_API_KEYenvironment variableFor maintainers
docker build .succeeded locallydocker compose upran successfully and app is reachable