Merge pull request #512 from presenton/fix/comfy_ui

fix: ComfyUI generation issue
This commit is contained in:
Shiva Raj Badu 2026-04-11 22:04:25 +05:45 committed by GitHub
commit 5c0c09e623
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 266 additions and 64 deletions

3
.gitignore vendored
View file

@ -20,4 +20,5 @@ container.db
.next-build
.cursor
.agents
skills-lock.json
skills-lock.json
.codex/

View file

@ -111,7 +111,9 @@ class ImageGenerationService:
"theme_prompt": prompt.theme_prompt,
},
)
elif image_path.startswith("/app_data/") or image_path.startswith("/static/"):
elif image_path.startswith("/app_data/") or image_path.startswith(
"/static/"
):
return self._to_frontend_url(image_path)
raise Exception(f"Image not found at {image_path}")
@ -174,14 +176,20 @@ class ImageGenerationService:
response_parts = getattr(response, "parts", None)
if not response_parts and getattr(response, "candidates", None):
first_candidate = response.candidates[0] if response.candidates else None
content = getattr(first_candidate, "content", None) if first_candidate else None
content = (
getattr(first_candidate, "content", None) if first_candidate else None
)
response_parts = getattr(content, "parts", None) if content else None
image_path = None
for part in response_parts or []:
if part.inline_data is not None:
mime_type = getattr(part.inline_data, "mime_type", "") or ""
ext = mime_type.split("/")[-1] if mime_type.startswith("image/") else "png"
ext = (
mime_type.split("/")[-1]
if mime_type.startswith("image/")
else "png"
)
image_path = os.path.join(output_directory, f"{uuid.uuid4()}.{ext}")
if hasattr(part, "as_image"):
part.as_image().save(image_path)
@ -368,28 +376,89 @@ class ImageGenerationService:
return image_path
def _inject_prompt_into_workflow(self, workflow: dict, prompt: str) -> dict:
"""
Find the prompt node in the workflow and inject the prompt text.
Looks for a node with title 'Input Prompt' (case-insensitive).
def norm(x) -> str:
return str(x or "").strip().lower()
User must rename their prompt node to 'Input Prompt' in ComfyUI.
"""
for node_id, node_data in workflow.items():
meta = node_data.get("_meta", {})
title = meta.get("title", "").lower()
def is_link(v) -> bool:
return (
isinstance(v, (list, tuple))
and len(v) >= 2
and isinstance(v[0], str)
and isinstance(v[1], int)
)
if title == "input prompt":
if "inputs" in node_data and "text" in node_data["inputs"]:
node_data["inputs"]["text"] = prompt
print(
f"Injected prompt into node {node_id}: {meta.get('title', '')}"
)
return workflow
preferred_keys = (
"text", "value", "prompt", "string", "content", "instruction", "input", "query"
)
# string inputs that are usually NOT prompt text
ignore_keys = {
"filename_prefix", "ckpt_name", "clip_name", "vae_name", "unet_name",
"sampler_name", "scheduler", "type", "device", "model", "lora_name"
}
visited = set()
def try_set(node_id: str) -> bool:
node_id = str(node_id)
if node_id in visited:
return False
visited.add(node_id)
node = workflow.get(node_id)
if not isinstance(node, dict):
return False
inputs = node.setdefault("inputs", {})
# 1) preferred prompt-like keys
for k in preferred_keys:
if k in inputs and isinstance(inputs[k], str):
inputs[k] = prompt
return True
# 2) fallback: exactly one unambiguous writable string field
string_candidates = [
k for k, v in inputs.items()
if isinstance(v, str) and k not in ignore_keys
]
if len(string_candidates) == 1:
inputs[string_candidates[0]] = prompt
return True
# 3) follow links from ANY input key (node-type agnostic)
for v in inputs.values():
if is_link(v):
if try_set(v[0]):
return True
elif isinstance(v, list):
for item in v:
if is_link(item) and try_set(item[0]):
return True
return False
input_prompt_nodes = [
node_id
for node_id, node_data in workflow.items()
if norm(node_data.get("_meta", {}).get("title")) == "input prompt"
]
if not input_prompt_nodes:
raise ValueError(
"Could not find node with title 'Input Prompt'. Rename your prompt node to 'Input Prompt'."
)
for nid in input_prompt_nodes:
if try_set(nid):
return workflow
raise ValueError(
"Could not find a node with title 'Input Prompt' in the workflow. Please rename your prompt node to 'Input Prompt' in ComfyUI."
"Found 'Input Prompt', but no writable prompt string field was found directly or through linked nodes."
)
async def _submit_comfyui_workflow(
self, session: aiohttp.ClientSession, comfyui_url: str, workflow: dict
) -> str:

View file

@ -121,10 +121,11 @@ const DashboardSidebar = () => {
aria-label={itemLabel}
title={itemLabel}
>
<div className="flex items-center ">
{/* <div className="flex items-center ">
<img src={imageProviderIcon} alt="image provider" className="w-5 h-5 rounded-full object-cover border border-[#EDEEEF]" />
<img src={textProviderIcon} alt="text provider" className="w-5 h-5 rounded-full object-cover border border-[#EDEEEF]" />
</div>
</div> */}
<Settings className={`h-4 w-4 ${isActive ? "text-[#5146E5]" : "text-slate-600"}`} />
<span className="text-[11px] text-slate-800">{itemLabel}</span>
</Link>
);

View file

@ -28,10 +28,10 @@ const Header = () => {
const backHref = backToUpload ? "/upload" : backToTemplates ? "/templates" : "/dashboard";
const backLabel = backToUpload
? "Back to upload"
? "Back"
: backToTemplates
? "Back to templates"
: "Go to your dashboard";
? "Back"
: "Back";
return (
<div className="w-full sticky top-0 z-50 py-7 "

View file

@ -80,7 +80,7 @@ const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF] flex items-center justify-center bg-white'>
<Shield className='w-3.5 h-3.5 text-[#5146E5]' />
</div>
<p className='text-[#191919] text-xs font-medium'>Privacy</p>
<p className='text-[#191919] text-xs font-medium'>Usage Analytics</p>
</button>
</div>
</div>

View file

@ -307,7 +307,7 @@ const PresentationHeader = ({
)}
>
{isEditingTitle ? (
<div className="flex items-stretch gap-0.5 rounded-[14px] border border-[#E4E2EB] bg-white pl-3.5 pr-1 py-1 shadow-[0_2px_12px_rgba(17,3,31,0.06)] ring-2 ring-[#5141e5]/15">
<div className="flex items-stretch w-[450px] gap-0.5 rounded-[14px] border border-[#E4E2EB] bg-white pl-3.5 pr-1 py-1 shadow-[0_2px_12px_rgba(17,3,31,0.06)] ring-2 ring-[#5141e5]/15">
<input
ref={titleInputRef}
value={draftTitle}
@ -364,7 +364,7 @@ const PresentationHeader = ({
"disabled:pointer-events-none disabled:opacity-100 disabled:hover:bg-transparent"
)}
>
<h2 className="min-w-0 flex-1 font-unbounded text-lg leading-snug text-[#101323]">
<h2 className="min-w-0 flex-1 font-unbounded text-lg w-[450px] leading-snug text-[#101323]">
<MarkdownRenderer
content={presentationData?.title || "Presentation"}
className="mb-0 min-w-0 overflow-hidden text-ellipsis line-clamp-1 text-sm text-[#101323] prose-p:my-0 prose-headings:my-0"

View file

@ -141,7 +141,7 @@ const SidePanel = ({
className="w-full h-[calc(100vh-120px)] hide-scrollbar overflow-hidden slide-theme "
>
<p className="text-xl font-normal pb-3.5 text-[#000000]">Slides ({presentationData?.slides?.length})</p>
<p className="text-xl font-normal font-syne pb-3.5 text-[#000000]">Slides ({presentationData?.slides?.length})</p>
<DndContext
sensors={sensors}

View file

@ -46,7 +46,7 @@ const page = () => {
<div className="relative">
<Header />
<div className="flex flex-col items-center justify-center mb-8">
<h1 className="text-[64px] font-normal font-unbounded text-[#101323] ">
<h1 className="text-[42px] font-normal font-unbounded text-[#101323] ">
Generate Presentation
</h1>
<p className="text-xl font-syne text-[#101323CC]">Choose a design, set preferences, and generate polished slides.</p>

View file

@ -24,6 +24,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { getApiUrl } from "@/utils/api";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
interface CodexConfigProps {
codexModel: string;
@ -118,6 +119,8 @@ export default function CodexConfig({
const handleSignIn = async () => {
try {
trackEvent(MixpanelEvent.Codex_SignIn_API_Call);
onInputChange('codex', 'LLM');
const res = await fetch(getApiUrl("/api/v1/ppt/codex/auth/initiate"), {

View file

@ -11,6 +11,7 @@ export enum MixpanelEvent {
Home_SaveConfiguration_API_Call = 'Home Save Configuration API Call',
Home_CheckOllamaModelPulled_API_Call = 'Home Check Ollama Model Pulled API Call',
Home_DownloadOllamaModel_API_Call = 'Home Download Ollama Model API Call',
Codex_SignIn_API_Call = 'Codex Sign In API Call',
Outline_Generate_Presentation_Button_Clicked = 'Outline Generate Presentation Button Clicked',
Outline_Select_Template_Button_Clicked = 'Outline Select Template Button Clicked',
Outline_Add_Slide_Button_Clicked = 'Outline Add Slide Button Clicked',

View file

@ -261,26 +261,85 @@ class ImageGenerationService:
return image_path
def _inject_prompt_into_workflow(self, workflow: dict, prompt: str) -> dict:
"""
Find the prompt node in the workflow and inject the prompt text.
Looks for a node with title 'Input Prompt' (case-insensitive).
def norm(x) -> str:
return str(x or "").strip().lower()
User must rename their prompt node to 'Input Prompt' in ComfyUI.
"""
for node_id, node_data in workflow.items():
meta = node_data.get("_meta", {})
title = meta.get("title", "").lower()
def is_link(v) -> bool:
return (
isinstance(v, (list, tuple))
and len(v) >= 2
and isinstance(v[0], str)
and isinstance(v[1], int)
)
if title == "input prompt":
if "inputs" in node_data and "text" in node_data["inputs"]:
node_data["inputs"]["text"] = prompt
print(
f"Injected prompt into node {node_id}: {meta.get('title', '')}"
)
return workflow
preferred_keys = (
"text", "value", "prompt", "string", "content", "instruction", "input", "query"
)
# string inputs that are usually NOT prompt text
ignore_keys = {
"filename_prefix", "ckpt_name", "clip_name", "vae_name", "unet_name",
"sampler_name", "scheduler", "type", "device", "model", "lora_name"
}
visited = set()
def try_set(node_id: str) -> bool:
node_id = str(node_id)
if node_id in visited:
return False
visited.add(node_id)
node = workflow.get(node_id)
if not isinstance(node, dict):
return False
inputs = node.setdefault("inputs", {})
# 1) preferred prompt-like keys
for k in preferred_keys:
if k in inputs and isinstance(inputs[k], str):
inputs[k] = prompt
return True
# 2) fallback: exactly one unambiguous writable string field
string_candidates = [
k for k, v in inputs.items()
if isinstance(v, str) and k not in ignore_keys
]
if len(string_candidates) == 1:
inputs[string_candidates[0]] = prompt
return True
# 3) follow links from ANY input key (node-type agnostic)
for v in inputs.values():
if is_link(v):
if try_set(v[0]):
return True
elif isinstance(v, list):
for item in v:
if is_link(item) and try_set(item[0]):
return True
return False
input_prompt_nodes = [
node_id
for node_id, node_data in workflow.items()
if norm(node_data.get("_meta", {}).get("title")) == "input prompt"
]
if not input_prompt_nodes:
raise ValueError(
"Could not find node with title 'Input Prompt'. Rename your prompt node to 'Input Prompt'."
)
for nid in input_prompt_nodes:
if try_set(nid):
return workflow
raise ValueError(
"Could not find a node with title 'Input Prompt' in the workflow. Please rename your prompt node to 'Input Prompt' in ComfyUI."
"Found 'Input Prompt', but no writable prompt string field was found directly or through linked nodes."
)
async def _submit_comfyui_workflow(

View file

@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@babel/standalone": "^7.28.2",
"@babel/traverse": "^7.29.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@ -117,12 +118,12 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@ -130,33 +131,56 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/generator": {
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"dev": true,
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.0"
"@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@ -183,15 +207,46 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0",
"debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true,
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@ -6605,6 +6660,18 @@
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"license": "MIT"
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",

View file

@ -11,6 +11,7 @@
},
"dependencies": {
"@babel/standalone": "^7.28.2",
"@babel/traverse": "^7.29.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",