xinjjj commited on
Commit
424804e
·
1 Parent(s): 2719ab7
app.py CHANGED
@@ -818,13 +818,13 @@ with gr.Blocks(
818
  <a href="https://horizonrobotics.github.io/EmbodiedGen">
819
  <img alt="📖 Documentation" src="https://img.shields.io/badge/📖-Documentation-blue">
820
  </a>
821
- <a href="https://arxiv.org/abs/2506.10600">
822
  <img alt="📄 arXiv" src="https://img.shields.io/badge/📄-arXiv-b31b1b">
823
  </a>
824
  <a href="https://github.com/HorizonRobotics/EmbodiedGen">
825
  <img alt="💻 GitHub" src="https://img.shields.io/badge/GitHub-000000?logo=github">
826
  </a>
827
- <a href="https://www.youtube.com/watch?v=rG4odybuJRk">
828
  <img alt="🎥 Video" src="https://img.shields.io/badge/🎥-Video-red">
829
  </a>
830
  </p>
 
818
  <a href="https://horizonrobotics.github.io/EmbodiedGen">
819
  <img alt="📖 Documentation" src="https://img.shields.io/badge/📖-Documentation-blue">
820
  </a>
821
+ <a href="https://arxiv.org/abs/2607.07459">
822
  <img alt="📄 arXiv" src="https://img.shields.io/badge/📄-arXiv-b31b1b">
823
  </a>
824
  <a href="https://github.com/HorizonRobotics/EmbodiedGen">
825
  <img alt="💻 GitHub" src="https://img.shields.io/badge/GitHub-000000?logo=github">
826
  </a>
827
+ <a href="https://youtu.be/MIkJJSVM8L4">
828
  <img alt="🎥 Video" src="https://img.shields.io/badge/🎥-Video-red">
829
  </a>
830
  </p>
app_style.py CHANGED
@@ -15,7 +15,7 @@
15
  # permissions and limitations under the License.
16
 
17
  from gradio.themes import Soft
18
- from gradio.themes.utils.colors import gray, neutral, slate, stone, teal, zinc
19
 
20
  lighting_css = """
21
  <style>
 
15
  # permissions and limitations under the License.
16
 
17
  from gradio.themes import Soft
18
+ from gradio.themes.utils.colors import gray, stone
19
 
20
  lighting_css = """
21
  <style>
embodied_gen/utils/gpt_clients.py CHANGED
@@ -16,10 +16,15 @@
16
 
17
 
18
  import base64
 
19
  import logging
20
  import math
21
  import os
 
 
 
22
  from io import BytesIO
 
23
  from typing import Optional
24
 
25
  import openai
@@ -43,12 +48,55 @@ __all__ = [
43
  "GPTclient",
44
  ]
45
 
46
- _CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
47
- CONFIG_FILE = os.path.join(_CURRENT_DIR, "gpt_config.yaml")
48
- DEFAULT_GPT_TIMEOUT = float(os.environ.get("GPT_TIMEOUT", 120))
49
  # GPT-5.x counts reasoning tokens against this cap, so it must be high
50
  # enough to leave room for both reasoning and the visible reply.
51
  GPT5_DEFAULT_MAX_COMPLETION_TOKENS = 8192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
 
54
  def combine_images_to_grid(
@@ -84,7 +132,7 @@ def combine_images_to_grid(
84
 
85
 
86
  class GPTclient:
87
- """A client to interact with GPT models via OpenAI or Azure API.
88
 
89
  Supports text and image prompts, connection checking, and configurable parameters.
90
 
@@ -96,6 +144,9 @@ class GPTclient:
96
  check_connection (bool, optional): Whether to check API connection.
97
  verbose (bool, optional): Enable verbose logging.
98
  timeout (float, optional): Max seconds for a single GPT request.
 
 
 
99
 
100
  Example:
101
  ```sh
@@ -117,15 +168,28 @@ class GPTclient:
117
 
118
  def __init__(
119
  self,
120
- endpoint: str,
121
- api_key: str,
122
- model_name: str = "yfb-gpt-4o",
123
- api_version: str = None,
124
  check_connection: bool = True,
125
  verbose: bool = False,
126
  timeout: float = DEFAULT_GPT_TIMEOUT,
 
127
  ):
128
- if api_version is not None:
 
 
 
 
 
 
 
 
 
 
 
 
129
  self.client = AzureOpenAI(
130
  azure_endpoint=endpoint,
131
  api_key=api_key,
@@ -151,6 +215,131 @@ class GPTclient:
151
 
152
  logger.info(f"Using GPT model: {self.model_name}.")
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  @staticmethod
155
  def _is_gpt5_model(model_name: str) -> bool:
156
  name = (model_name or "").lower()
@@ -168,7 +357,9 @@ class GPTclient:
168
  def query(
169
  self,
170
  text_prompt: str,
171
- image_base64: Optional[list[str | Image.Image]] = None,
 
 
172
  system_role: Optional[str] = None,
173
  params: Optional[dict] = None,
174
  ) -> Optional[str]:
@@ -186,6 +377,23 @@ class GPTclient:
186
  if system_role is None:
187
  system_role = "You are a highly knowledgeable assistant specializing in physics, engineering, and object properties." # noqa
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  content_user = [
190
  {
191
  "type": "text",
@@ -292,6 +500,20 @@ class GPTclient:
292
  ConnectionError: If connection fails.
293
  """
294
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  probe_kwargs = dict(
296
  messages=[
297
  {"role": "system", "content": "You are a test system."},
@@ -314,26 +536,19 @@ class GPTclient:
314
  )
315
 
316
 
317
- with open(CONFIG_FILE, "r") as f:
318
  config = yaml.safe_load(f)
319
 
320
- agent_type = config["agent_type"]
321
- agent_config = config.get(agent_type, {})
322
-
323
- # Prefer environment variables, fallback to YAML config
324
- endpoint = os.environ.get("ENDPOINT", agent_config.get("endpoint"))
325
- api_key = os.environ.get("API_KEY", agent_config.get("api_key"))
326
- api_version = os.environ.get("API_VERSION", agent_config.get("api_version"))
327
- model_name = os.environ.get("MODEL_NAME", agent_config.get("model_name"))
328
- timeout = DEFAULT_GPT_TIMEOUT
329
 
330
  GPT_CLIENT = GPTclient(
331
- endpoint=endpoint,
332
- api_key=api_key,
333
- api_version=api_version,
334
- model_name=model_name,
335
  check_connection=False,
336
- timeout=timeout,
 
337
  )
338
 
339
 
 
16
 
17
 
18
  import base64
19
+ import json
20
  import logging
21
  import math
22
  import os
23
+ import shutil
24
+ import subprocess
25
+ import tempfile
26
  from io import BytesIO
27
+ from pathlib import Path
28
  from typing import Optional
29
 
30
  import openai
 
48
  "GPTclient",
49
  ]
50
 
51
+ CONFIG_FILE = str(Path(__file__).with_name("gpt_config.yaml"))
52
+ DEFAULT_GPT_TIMEOUT = float(os.environ.get("GPT_TIMEOUT", 90))
 
53
  # GPT-5.x counts reasoning tokens against this cap, so it must be high
54
  # enough to leave room for both reasoning and the visible reply.
55
  GPT5_DEFAULT_MAX_COMPLETION_TOKENS = 8192
56
+ _CODEX_DEFAULT_REASONING_EFFORT = "medium"
57
+ _CODEX_ENV_KEYS = {
58
+ "ALL_PROXY",
59
+ "CODEX_HOME",
60
+ "HOME",
61
+ "HTTPS_PROXY",
62
+ "HTTP_PROXY",
63
+ "NO_PROXY",
64
+ "PATH",
65
+ }
66
+
67
+
68
+ def _resolve_agent_settings(
69
+ config: dict, environ: Optional[dict[str, str]] = None
70
+ ) -> dict:
71
+ """Resolve one provider configuration with environment overrides."""
72
+ environ = os.environ if environ is None else environ
73
+ agent_type = config["agent_type"]
74
+ agent_config = config.get(agent_type, {})
75
+ provider_override = environ.get("GPT_PROVIDER")
76
+
77
+ if provider_override is not None:
78
+ agent_config = {}
79
+
80
+ return {
81
+ "endpoint": environ.get("ENDPOINT", agent_config.get("endpoint")),
82
+ "api_key": environ.get("API_KEY", agent_config.get("api_key")),
83
+ "api_version": environ.get(
84
+ "API_VERSION", agent_config.get("api_version")
85
+ ),
86
+ "model_name": environ.get(
87
+ "MODEL_NAME", agent_config.get("model_name")
88
+ ),
89
+ "provider": provider_override or agent_config.get("provider"),
90
+ }
91
+
92
+
93
+ def _codex_subprocess_environment() -> dict[str, str]:
94
+ """Return the minimal host environment required by Codex CLI."""
95
+ return {
96
+ key: value
97
+ for key, value in os.environ.items()
98
+ if key.upper() in _CODEX_ENV_KEYS
99
+ }
100
 
101
 
102
  def combine_images_to_grid(
 
132
 
133
 
134
  class GPTclient:
135
+ """A client to interact with GPT models via API or Codex CLI.
136
 
137
  Supports text and image prompts, connection checking, and configurable parameters.
138
 
 
144
  check_connection (bool, optional): Whether to check API connection.
145
  verbose (bool, optional): Enable verbose logging.
146
  timeout (float, optional): Max seconds for a single GPT request.
147
+ provider (str, optional): Backend provider. Use ``codex`` to reuse a
148
+ local Codex CLI login; otherwise the existing Azure/OpenAI-
149
+ compatible API selection is preserved.
150
 
151
  Example:
152
  ```sh
 
168
 
169
  def __init__(
170
  self,
171
+ endpoint: Optional[str],
172
+ api_key: Optional[str],
173
+ model_name: Optional[str] = "yfb-gpt-4o",
174
+ api_version: Optional[str] = None,
175
  check_connection: bool = True,
176
  verbose: bool = False,
177
  timeout: float = DEFAULT_GPT_TIMEOUT,
178
+ provider: Optional[str] = None,
179
  ):
180
+ self.provider = (
181
+ provider or ("azure" if api_version else "openai")
182
+ ).lower()
183
+ self.codex_executable = None
184
+ if self.provider == "codex":
185
+ self.codex_executable = shutil.which("codex")
186
+ if self.codex_executable is None:
187
+ raise RuntimeError(
188
+ "Codex CLI was not found. Install Codex and run "
189
+ "`codex login` before using the Codex provider."
190
+ )
191
+ self.client = None
192
+ elif self.provider == "azure" or api_version is not None:
193
  self.client = AzureOpenAI(
194
  azure_endpoint=endpoint,
195
  api_key=api_key,
 
215
 
216
  logger.info(f"Using GPT model: {self.model_name}.")
217
 
218
+ def _materialize_codex_image(
219
+ self,
220
+ image: str | Image.Image,
221
+ target_stem: Path,
222
+ ) -> Path:
223
+ """Normalize one Codex image input to a temporary PNG file."""
224
+ target = target_stem.with_suffix(".png")
225
+ if isinstance(image, Image.Image):
226
+ image.convert("RGB").save(target, format="PNG")
227
+ return target
228
+
229
+ if not isinstance(image, str):
230
+ raise TypeError(
231
+ "Codex image input must be a path, base64 string, or PIL Image"
232
+ )
233
+
234
+ if not image.startswith("data:"):
235
+ source = Path(image).expanduser()
236
+ try:
237
+ source_is_file = source.is_file()
238
+ except OSError:
239
+ source_is_file = False
240
+ if source_is_file:
241
+ try:
242
+ with Image.open(source) as source_image:
243
+ source_image.convert("RGB").save(target, format="PNG")
244
+ except OSError as exc:
245
+ raise ValueError(f"Invalid image file: {image}") from exc
246
+ return target
247
+ if source.suffix.lower() in self.image_formats:
248
+ raise FileNotFoundError(f"Image file not found: {image}")
249
+ encoded = image
250
+ else:
251
+ header, separator, encoded = image.partition(",")
252
+ if not separator or ";base64" not in header.lower():
253
+ raise ValueError("Image data URI must contain base64 data")
254
+
255
+ try:
256
+ image_data = base64.b64decode(encoded, validate=True)
257
+ with Image.open(BytesIO(image_data)) as decoded_image:
258
+ decoded_image.convert("RGB").save(target, format="PNG")
259
+ except (OSError, ValueError) as exc:
260
+ raise ValueError(
261
+ "Codex image input is neither an existing image nor valid base64"
262
+ ) from exc
263
+ return target
264
+
265
+ def _query_codex(
266
+ self,
267
+ text_prompt: str,
268
+ image_base64: Optional[str | Image.Image | list[str | Image.Image]],
269
+ system_role: str,
270
+ params: Optional[dict],
271
+ ) -> str:
272
+ """Run one non-interactive Codex CLI request."""
273
+ params = params or {}
274
+ with tempfile.TemporaryDirectory(prefix="embodiedgen-codex-") as tmp:
275
+ tmp_path = Path(tmp)
276
+ output_path = tmp_path / "response.txt"
277
+ image_paths = []
278
+
279
+ images = image_base64 or []
280
+ if not isinstance(images, list):
281
+ images = [images]
282
+ for index, image in enumerate(images):
283
+ image_paths.append(
284
+ self._materialize_codex_image(
285
+ image,
286
+ tmp_path / f"image_{index}",
287
+ )
288
+ )
289
+
290
+ prompt = f"{system_role}\n\nUser request:\n{text_prompt}"
291
+ command = [
292
+ self.codex_executable,
293
+ "exec",
294
+ "--ephemeral",
295
+ "--sandbox",
296
+ "read-only",
297
+ "--skip-git-repo-check",
298
+ "--ignore-user-config",
299
+ "--ignore-rules",
300
+ "--color",
301
+ "never",
302
+ "--output-last-message",
303
+ str(output_path),
304
+ "--cd",
305
+ str(tmp_path),
306
+ ]
307
+ model_name = params.get("model", self.model_name)
308
+ if model_name:
309
+ command.extend(["--model", model_name])
310
+ reasoning_effort = params.get(
311
+ "model_reasoning_effort", _CODEX_DEFAULT_REASONING_EFFORT
312
+ )
313
+ if not isinstance(reasoning_effort, str) or not reasoning_effort:
314
+ raise ValueError(
315
+ "model_reasoning_effort must be a non-empty string"
316
+ )
317
+ command.extend(
318
+ [
319
+ "--config",
320
+ f"model_reasoning_effort={json.dumps(reasoning_effort)}",
321
+ ]
322
+ )
323
+ for image_path in image_paths:
324
+ command.extend(["--image", str(image_path)])
325
+ command.append("-")
326
+
327
+ result = subprocess.run(
328
+ command,
329
+ input=prompt,
330
+ encoding="utf-8",
331
+ capture_output=True,
332
+ timeout=self.timeout,
333
+ check=False,
334
+ env=_codex_subprocess_environment(),
335
+ )
336
+ if result.returncode != 0:
337
+ raise RuntimeError(result.stderr.strip() or "Codex CLI failed")
338
+ response = output_path.read_text(encoding="utf-8").strip()
339
+ if not response:
340
+ raise RuntimeError("Codex CLI returned an empty response")
341
+ return response
342
+
343
  @staticmethod
344
  def _is_gpt5_model(model_name: str) -> bool:
345
  name = (model_name or "").lower()
 
357
  def query(
358
  self,
359
  text_prompt: str,
360
+ image_base64: Optional[
361
+ str | Image.Image | list[str | Image.Image]
362
+ ] = None,
363
  system_role: Optional[str] = None,
364
  params: Optional[dict] = None,
365
  ) -> Optional[str]:
 
377
  if system_role is None:
378
  system_role = "You are a highly knowledgeable assistant specializing in physics, engineering, and object properties." # noqa
379
 
380
+ if self.provider == "codex":
381
+ try:
382
+ response = self._query_codex(
383
+ text_prompt=text_prompt,
384
+ image_base64=image_base64,
385
+ system_role=system_role,
386
+ params=params,
387
+ )
388
+ except Exception as e:
389
+ logger.error(f"Error Codex CLI call: {e}")
390
+ response = None
391
+
392
+ if self.verbose:
393
+ logger.info(f"Prompt: {text_prompt}")
394
+ logger.info(f"Response: {response}")
395
+ return response
396
+
397
  content_user = [
398
  {
399
  "type": "text",
 
500
  ConnectionError: If connection fails.
501
  """
502
  try:
503
+ if self.provider == "codex":
504
+ response = self._query_codex(
505
+ text_prompt="Reply with OK.",
506
+ image_base64=None,
507
+ system_role="You are a test system.",
508
+ params=None,
509
+ )
510
+ if not response:
511
+ raise ConnectionError(
512
+ "Codex CLI returned an empty response"
513
+ )
514
+ logger.info("Connection check success.")
515
+ return
516
+
517
  probe_kwargs = dict(
518
  messages=[
519
  {"role": "system", "content": "You are a test system."},
 
536
  )
537
 
538
 
539
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
540
  config = yaml.safe_load(f)
541
 
542
+ settings = _resolve_agent_settings(config)
 
 
 
 
 
 
 
 
543
 
544
  GPT_CLIENT = GPTclient(
545
+ endpoint=settings["endpoint"],
546
+ api_key=settings["api_key"],
547
+ api_version=settings["api_version"],
548
+ model_name=settings["model_name"],
549
  check_connection=False,
550
+ timeout=DEFAULT_GPT_TIMEOUT,
551
+ provider=settings["provider"],
552
  )
553
 
554
 
embodied_gen/utils/gpt_config.yaml CHANGED
@@ -1,5 +1,5 @@
1
  # config.yaml
2
- agent_type: "gpt-5.4" # gpt-4o, gpt-5.4 or gemma-4-31b
3
 
4
  gpt-4o:
5
  endpoint: https://xxx.openai.azure.com
@@ -18,3 +18,10 @@ gemma-4-31b:
18
  api_key: sk-or-v1-xxx
19
  api_version: null
20
  model_name: google/gemma-4-31b-it:free
 
 
 
 
 
 
 
 
1
  # config.yaml
2
+ agent_type: "gpt-5.4" # gpt-4o, gpt-5.4 or gemma-4-31b or codex
3
 
4
  gpt-4o:
5
  endpoint: https://xxx.openai.azure.com
 
18
  api_key: sk-or-v1-xxx
19
  api_version: null
20
  model_name: google/gemma-4-31b-it:free
21
+
22
+ codex:
23
+ provider: codex
24
+ endpoint: null
25
+ api_key: null
26
+ api_version: null
27
+ model_name: null
embodied_gen/utils/tags.py CHANGED
@@ -1 +1 @@
1
- VERSION = "v2.0.0"
 
1
+ VERSION = "v2.1.0"