feat: application des chantiers C1-C5 au moteur, facilitator et catalogue
This commit is contained in:
+264
-22
@@ -63,12 +63,14 @@ API_KEY = os.getenv("MISTRAL_API_KEY")
|
||||
NARRATOR_ID = os.getenv("NARRATOR_AGENT_ID")
|
||||
DESIGNER_ID = os.getenv("DESIGNER_AGENT_ID")
|
||||
ENCODER_ID = os.getenv("ENCODER_AGENT_ID")
|
||||
ENCODER_MODE = os.getenv("ENCODER_MODE", "agent") # agent | schema (C1)
|
||||
FREE_DESIGNER_ID = os.getenv("FREE_DESIGNER_AGENT_ID")
|
||||
PROJECTS_DIR = Path(os.getenv("PROJECTS_DIR", "./projets"))
|
||||
RENDER_ENGINE_PATH = os.getenv("RENDER_ENGINE_PATH", "render_engine_v2.py")
|
||||
THEME_PATH = os.getenv("THEME_PATH", "theme_v2.yaml")
|
||||
COMPONENTS_PATH = os.getenv("COMPONENTS_PATH", "components_v2.yaml")
|
||||
LAYOUTS_PATH = os.getenv("LAYOUTS_PATH", "layouts_v2.yaml")
|
||||
PREVIEW_SCRIPT = os.getenv("PREVIEW_SCRIPT", "./preview.sh") # C2
|
||||
CONTEXT_MAX_CHARS = int(os.getenv("CONTEXT_MAX_CHARS", "12000"))
|
||||
TRILIUM_API_URL = os.getenv("TRILIUM_API_URL", "")
|
||||
TRILIUM_API_KEY = os.getenv("TRILIUM_API_KEY", "")
|
||||
@@ -92,6 +94,7 @@ SLASH_CMDS = {
|
||||
"/formalise": "Transformer le plan compact validé en Markdown pour le Designer",
|
||||
"/valider": "Valider le Markdown formalisé → passer au Designer",
|
||||
"/plan": "Réafficher le dernier plan compact en entier",
|
||||
"/afficher": "Réafficher la dernière réponse du Narrator en entier (sans troncature)",
|
||||
"/aide": "Afficher cette aide",
|
||||
"/quitter": "Abandonner et revenir au menu projet",
|
||||
}
|
||||
@@ -678,8 +681,13 @@ def run_narrator(session: AgentSession, proj: Project) -> Optional[str]:
|
||||
|
||||
elif cmd == "/lire":
|
||||
context, new_files = load_documents(proj, loaded_docs)
|
||||
if not new_files:
|
||||
info("Aucun nouveau document dans inputs/.")
|
||||
assets_info = list_assets(proj)
|
||||
if assets_info:
|
||||
context = ((context or "").strip()
|
||||
+ "\n\n" + assets_info).strip()
|
||||
ok(f"{len(assets_info.splitlines()) - 1} image(s) dans assets/.")
|
||||
if not new_files and not assets_info:
|
||||
info("Aucun nouveau document dans inputs/ ni image dans assets/.")
|
||||
continue
|
||||
loaded_docs |= new_files
|
||||
if not session.started:
|
||||
@@ -718,6 +726,13 @@ def run_narrator(session: AgentSession, proj: Project) -> Optional[str]:
|
||||
warn("Aucun plan compact pour l'instant.")
|
||||
continue
|
||||
|
||||
elif cmd == "/afficher":
|
||||
if last_response:
|
||||
print("\n" + textwrap.indent(last_response, " "))
|
||||
else:
|
||||
warn("Pas encore de réponse à afficher.")
|
||||
continue
|
||||
|
||||
elif cmd == "/formalise":
|
||||
if not session.started:
|
||||
warn("Commence par établir le plan compact."); continue
|
||||
@@ -994,6 +1009,133 @@ def run_encoder(session: AgentSession, plan: str, layouts: dict,
|
||||
return None, None, None, attempts
|
||||
|
||||
|
||||
|
||||
def run_encoder_schema(plan: str, layouts: dict, proj: "Project"):
|
||||
"""Encoder structuré (chantier C1) : chat/completions + json_schema
|
||||
strict Mistral, slide par slide. Même contrat de retour que
|
||||
run_encoder : (yaml_str, data, path, attempts) — attempts = nb de
|
||||
slides en échec. Fallback automatique sur l'Encoder agent si le
|
||||
module manque ou si aucune slide n'est encodée."""
|
||||
section("ÉTAPE 3 — THE ENCODER (structured outputs)")
|
||||
try:
|
||||
import encoder_schema as enc
|
||||
except ImportError as e:
|
||||
warn(f"encoder_schema.py indisponible ({e}) — bascule mode agent.")
|
||||
return run_encoder(AgentSession(ENCODER_ID), plan, layouts, proj)
|
||||
try:
|
||||
import schemas as sch
|
||||
for issue in (sch.verify_against_layouts(layouts) if layouts else []):
|
||||
warn(f"Schéma vs layouts_v2 : {issue}")
|
||||
except ImportError:
|
||||
warn("schemas.py absent — vérification de cohérence sautée.")
|
||||
info(f"Encodage slide par slide ({enc.DEFAULT_MODEL}, temp 0)...")
|
||||
data, usage, errors = enc.encode_plan(plan, API_KEY, progress=info)
|
||||
nb = len(data.get("slides", []))
|
||||
ok(f"{nb} slides encodées — tokens : {usage.get('total_tokens', 0)} "
|
||||
f"(prompt {usage.get('prompt_tokens', 0)} / "
|
||||
f"completion {usage.get('completion_tokens', 0)})")
|
||||
for e in errors:
|
||||
warn(e)
|
||||
if not nb:
|
||||
warn("Aucune slide encodée — bascule mode agent.")
|
||||
return run_encoder(AgentSession(ENCODER_ID), plan, layouts, proj)
|
||||
yaml_str = enc.to_yaml(data)
|
||||
is_valid, message, _ = validate_yaml(yaml_str, layouts)
|
||||
if is_valid:
|
||||
ok(f"YAML valide : {message}")
|
||||
else:
|
||||
warn(f"Validation : {message}")
|
||||
if errors:
|
||||
print("\n [1] Continuer sans les slides en échec")
|
||||
print(" [2] Basculer sur l'Encoder agent (deck complet)")
|
||||
print(" [0] Abandonner")
|
||||
choix = ask("Votre choix :")
|
||||
if choix == "2":
|
||||
return run_encoder(AgentSession(ENCODER_ID), plan, layouts, proj)
|
||||
if choix == "0":
|
||||
return None, None, None, len(errors)
|
||||
path = save_text(yaml_str, proj.out("input", "yaml"))
|
||||
return yaml_str, data, path, len(errors)
|
||||
|
||||
|
||||
|
||||
def merge_revision(proj: "Project", partial_yaml_path: Path):
|
||||
"""Fusion YAML des révisions ciblées (chantier C3).
|
||||
Remplace dans le dernier YAML complet les slides régénérées
|
||||
(appariement par position ; positions inconnues ajoutées en fin).
|
||||
Retourne le chemin du YAML complet fusionné, ou None si fusion
|
||||
impossible (l'appelant conserve alors le flux partiel actuel)."""
|
||||
state = proj.load_state()
|
||||
last = state.get("dernier_yaml") or ""
|
||||
if not last or not Path(last).exists():
|
||||
warn("Fusion : pas de YAML complet précédent — PPTX partiel "
|
||||
"conservé.")
|
||||
return None
|
||||
try:
|
||||
full = yaml.safe_load(Path(last).read_text(encoding="utf-8"))
|
||||
part = yaml.safe_load(
|
||||
partial_yaml_path.read_text(encoding="utf-8"))
|
||||
except yaml.YAMLError as e:
|
||||
warn(f"Fusion : YAML illisible ({e}).")
|
||||
return None
|
||||
if not isinstance(full, dict) or not full.get("slides"):
|
||||
warn("Fusion : le YAML précédent ne contient pas de slides.")
|
||||
return None
|
||||
news = {}
|
||||
for s in (part or {}).get("slides", []):
|
||||
if isinstance(s, dict) and s.get("position"):
|
||||
news[int(s["position"])] = s
|
||||
if not news:
|
||||
warn("Fusion : aucune slide positionnée dans la révision.")
|
||||
return None
|
||||
merged, replaced = [], 0
|
||||
for i, s in enumerate(full["slides"]):
|
||||
pos = int(s.get("position", i + 1)) if isinstance(s, dict) else i + 1
|
||||
if pos in news:
|
||||
merged.append(news.pop(pos))
|
||||
replaced += 1
|
||||
else:
|
||||
merged.append(s)
|
||||
for pos in sorted(news):
|
||||
merged.append(news[pos])
|
||||
full["slides"] = merged
|
||||
out = proj.out("revision_fusion", "yaml")
|
||||
out.write_text(
|
||||
yaml.safe_dump(full, allow_unicode=True, sort_keys=False,
|
||||
default_flow_style=False, width=100),
|
||||
encoding="utf-8")
|
||||
ok(f"Fusion : {replaced} slide(s) remplacée(s), "
|
||||
f"{len(merged)} au total → {out.name}")
|
||||
return out
|
||||
|
||||
|
||||
|
||||
def list_assets(proj: "Project"):
|
||||
"""Inventaire des images de projets/<slug>/assets/ (chantier C4).
|
||||
Crée le dossier au premier appel. Retourne un bloc texte destiné au
|
||||
contexte Narrator, ou une chaîne vide si aucune image."""
|
||||
assets = proj.root / "assets"
|
||||
assets.mkdir(exist_ok=True)
|
||||
exts = {".png", ".jpg", ".jpeg", ".webp"}
|
||||
files = sorted(p for p in assets.iterdir()
|
||||
if p.suffix.lower() in exts and p.is_file())
|
||||
if not files:
|
||||
return ""
|
||||
lines = ["IMAGES DISPONIBLES DANS assets/ (utilisables dans les "
|
||||
"layouts image_split / image_full et le bloc freeform "
|
||||
"image, par leur nom de fichier) :"]
|
||||
for p in files:
|
||||
dims = ""
|
||||
try:
|
||||
from PIL import Image
|
||||
with Image.open(p) as im:
|
||||
dims = f" ({im.size[0]}×{im.size[1]})"
|
||||
except Exception:
|
||||
pass
|
||||
lines.append(f"- {p.name}{dims}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# FLUX LIBRE — THE FREE DESIGNER
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -1001,7 +1143,7 @@ def run_encoder(session: AgentSession, plan: str, layouts: dict,
|
||||
VALID_TOKENS = {"navy", "navy_light", "coral", "glacier", "slate",
|
||||
"card", "white", "body", "muted"}
|
||||
VALID_BLOCK_TYPES = {"title", "heading", "text", "stat", "circle",
|
||||
"badge", "card", "rect", "line"}
|
||||
"badge", "card", "rect", "line", "image"}
|
||||
|
||||
|
||||
def validate_freeform(raw: str):
|
||||
@@ -1112,7 +1254,8 @@ def run_free_designer(session: AgentSession, markdown: str, proj: Project):
|
||||
# ÉTAPE 4 — RENDER ENGINE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def run_render(yaml_path: Path) -> Optional[Path]:
|
||||
def run_render(yaml_path: Path,
|
||||
assets_dir: Optional[Path] = None) -> Optional[Path]:
|
||||
section("ÉTAPE 4 — RENDER ENGINE V2")
|
||||
if not Path(RENDER_ENGINE_PATH).exists():
|
||||
warn(f"{RENDER_ENGINE_PATH} introuvable.")
|
||||
@@ -1124,11 +1267,22 @@ def run_render(yaml_path: Path) -> Optional[Path]:
|
||||
pptx_out = yaml_path.with_suffix(".pptx")
|
||||
info("Lancement de render_engine_v2.py...")
|
||||
info(f"Sortie : {pptx_out}")
|
||||
result = subprocess.run(
|
||||
[sys.executable, RENDER_ENGINE_PATH, str(yaml_path), str(pptx_out),
|
||||
"--theme", THEME_PATH, "--components", COMPONENTS_PATH,
|
||||
"--layouts", LAYOUTS_PATH],
|
||||
capture_output=True, text=True)
|
||||
if assets_dir is None:
|
||||
# Déduction depuis PROJECTS_DIR si le YAML y vit (C4b)
|
||||
try:
|
||||
rel = yaml_path.resolve().relative_to(
|
||||
Path(PROJECTS_DIR).resolve())
|
||||
assets_dir = Path(PROJECTS_DIR) / rel.parts[0] / "assets"
|
||||
except ValueError:
|
||||
assets_dir = None # hors projet : défaut du moteur
|
||||
cmd = [sys.executable, RENDER_ENGINE_PATH, str(yaml_path),
|
||||
str(pptx_out),
|
||||
"--theme", THEME_PATH, "--components", COMPONENTS_PATH,
|
||||
"--layouts", LAYOUTS_PATH]
|
||||
if assets_dir is not None:
|
||||
cmd += ["--assets", str(assets_dir)]
|
||||
info(f"Assets : {assets_dir}")
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
ok(result.stdout.strip() or f"PPTX généré : {pptx_out}")
|
||||
return pptx_out
|
||||
@@ -1137,6 +1291,46 @@ def run_render(yaml_path: Path) -> Optional[Path]:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def run_preview(pptx_path: Path, wait: bool = False) -> bool:
|
||||
"""Aperçus PNG par slide via preview.sh (chantier C2).
|
||||
wait=False : lancement en arrière-plan (le DS218 est lent), sortie
|
||||
consignée dans <pptx>_preview.log. wait=True : bloquant (CLI)."""
|
||||
script = Path(PREVIEW_SCRIPT).resolve()
|
||||
if not script.exists():
|
||||
warn(f"{PREVIEW_SCRIPT} introuvable — aperçus indisponibles.")
|
||||
return False
|
||||
out_dir = pptx_path.parent / f"{pptx_path.stem}_previews"
|
||||
if wait:
|
||||
info("Génération des aperçus (quelques minutes sur le NAS)...")
|
||||
r = subprocess.run([str(script), str(pptx_path)],
|
||||
capture_output=True, text=True)
|
||||
if r.returncode == 0:
|
||||
ok(f"Aperçus : {out_dir}")
|
||||
ok(f"Galerie : {out_dir / 'index.html'}")
|
||||
return True
|
||||
warn("Échec de la génération des aperçus :")
|
||||
print(textwrap.indent((r.stderr or r.stdout or "?").strip(),
|
||||
" "))
|
||||
return False
|
||||
log = pptx_path.parent / f"{pptx_path.stem}_preview.log"
|
||||
with open(log, "w", encoding="utf-8") as lf:
|
||||
subprocess.Popen([str(script), str(pptx_path)],
|
||||
stdout=lf, stderr=subprocess.STDOUT)
|
||||
info(f"Aperçus en arrière-plan → {out_dir}")
|
||||
info(f"Suivi : {log}")
|
||||
return True
|
||||
|
||||
|
||||
def maybe_preview(pptx_path) -> None:
|
||||
"""Propose la génération des aperçus après une sortie PPTX."""
|
||||
if not pptx_path or not Path(PREVIEW_SCRIPT).exists():
|
||||
return
|
||||
if ask("Générer les aperçus PNG ? (o/N) :").lower() in (
|
||||
"o", "oui", "y", "yes"):
|
||||
run_preview(pptx_path, wait=False)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ARCHIVAGE TRILIUM
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -1212,9 +1406,13 @@ def run_full_pipeline_pass(narrator, markdown, layouts, proj, manifest,
|
||||
continue
|
||||
manifest.step("designer", caracteres=len(plan))
|
||||
|
||||
encoder = AgentSession(ENCODER_ID)
|
||||
yaml_str, yaml_data, yaml_path, tries = run_encoder(
|
||||
encoder, plan, layouts, proj, scope_encoder)
|
||||
if ENCODER_MODE == "schema":
|
||||
yaml_str, yaml_data, yaml_path, tries = run_encoder_schema(
|
||||
plan, layouts, proj)
|
||||
else:
|
||||
encoder = AgentSession(ENCODER_ID)
|
||||
yaml_str, yaml_data, yaml_path, tries = run_encoder(
|
||||
encoder, plan, layouts, proj, scope_encoder)
|
||||
manifest.step("encoder", tentatives_correction=tries)
|
||||
if yaml_str is None:
|
||||
info("Retour au Designer...")
|
||||
@@ -1226,8 +1424,15 @@ def run_full_pipeline_pass(narrator, markdown, layouts, proj, manifest,
|
||||
yaml_path.rename(new_path)
|
||||
yaml_path = new_path
|
||||
|
||||
merged = False
|
||||
if suffix == "revision_ciblee" and yaml_path:
|
||||
fused = merge_revision(proj, yaml_path)
|
||||
if fused:
|
||||
yaml_path, merged = fused, True
|
||||
info("Rendu du deck COMPLET fusionné.")
|
||||
|
||||
manifest.file(yaml_path)
|
||||
pptx_path = run_render(yaml_path)
|
||||
pptx_path = run_render(yaml_path, proj.root / "assets")
|
||||
manifest.step("render", succes=bool(pptx_path))
|
||||
# Persister l'état du projet (seulement en révision complète :
|
||||
# le ciblé ne représente pas l'état complet du deck)
|
||||
@@ -1237,9 +1442,15 @@ def run_full_pipeline_pass(narrator, markdown, layouts, proj, manifest,
|
||||
pptx_path=pptx_path,
|
||||
journal_entry="Révision structurante — deck complet régénéré.")
|
||||
elif suffix == "revision_ciblee":
|
||||
persist_generation(
|
||||
proj, markdown=markdown,
|
||||
journal_entry="Révision ciblée — slides régénérées séparément.")
|
||||
if merged:
|
||||
persist_generation(
|
||||
proj, markdown=markdown, yaml_path=yaml_path,
|
||||
pptx_path=pptx_path,
|
||||
journal_entry="Révision ciblée fusionnée — deck complet régénéré.")
|
||||
else:
|
||||
persist_generation(
|
||||
proj, markdown=markdown,
|
||||
journal_entry="Révision ciblée — slides régénérées séparément.")
|
||||
return pptx_path, markdown
|
||||
|
||||
|
||||
@@ -1304,6 +1515,8 @@ def run_revision(narrator, last_markdown, layouts, proj, manifest):
|
||||
section("SLIDES RÉVISÉES GÉNÉRÉES")
|
||||
ok(f"PPTX des slides révisées : {pptx_path}")
|
||||
info("Ouvre ce fichier et copie-colle les slides dans ton deck maître.")
|
||||
info("(Si la fusion YAML a réussi — voir ci-dessus — le PPTX est déjà le deck complet.)")
|
||||
maybe_preview(pptx_path)
|
||||
else:
|
||||
warn("Les slides révisées n'ont pas pu être générées.")
|
||||
return new_markdown
|
||||
@@ -1316,6 +1529,7 @@ def run_revision(narrator, last_markdown, layouts, proj, manifest):
|
||||
if pptx_path and pptx_path.exists():
|
||||
section("DECK COMPLET RÉGÉNÉRÉ")
|
||||
ok(f"Nouveau PPTX complet : {pptx_path}")
|
||||
maybe_preview(pptx_path)
|
||||
else:
|
||||
warn("Le deck n'a pas pu être régénéré.")
|
||||
return new_markdown
|
||||
@@ -1375,13 +1589,14 @@ def run_creation(proj, layouts):
|
||||
|
||||
if yaml_path and yaml_data:
|
||||
manifest.file(yaml_path)
|
||||
pptx_path = run_render(yaml_path)
|
||||
pptx_path = run_render(yaml_path, proj.root / "assets")
|
||||
manifest.step("render", succes=bool(pptx_path))
|
||||
if pptx_path and pptx_path.exists():
|
||||
manifest.file(pptx_path)
|
||||
section("PRÉSENTATION GÉNÉRÉE (flux libre)")
|
||||
ok(f"Fichier PPTX : {pptx_path}")
|
||||
ok(f"Taille : {pptx_path.stat().st_size/1024:.1f} Ko")
|
||||
maybe_preview(pptx_path)
|
||||
else:
|
||||
warn("Le PPTX n'a pas pu être généré.")
|
||||
persist_generation(
|
||||
@@ -1420,9 +1635,15 @@ def run_creation(proj, layouts):
|
||||
continue
|
||||
manifest.step("designer", caracteres=len(plan))
|
||||
|
||||
encoder = AgentSession(ENCODER_ID)
|
||||
yaml_str, yaml_data, yaml_path, tries = run_encoder(
|
||||
encoder, plan, layouts, proj)
|
||||
if ENCODER_MODE == "schema" and not express:
|
||||
yaml_str, yaml_data, yaml_path, tries = run_encoder_schema(
|
||||
plan, layouts, proj)
|
||||
else:
|
||||
if ENCODER_MODE == "schema" and express:
|
||||
info("Mode express → Encoder agent (plan non annoté).")
|
||||
encoder = AgentSession(ENCODER_ID)
|
||||
yaml_str, yaml_data, yaml_path, tries = run_encoder(
|
||||
encoder, plan, layouts, proj)
|
||||
manifest.step("encoder", tentatives_correction=tries)
|
||||
if yaml_str is None:
|
||||
if express:
|
||||
@@ -1434,13 +1655,14 @@ def run_creation(proj, layouts):
|
||||
|
||||
if yaml_path and yaml_data:
|
||||
manifest.file(yaml_path)
|
||||
pptx_path = run_render(yaml_path)
|
||||
pptx_path = run_render(yaml_path, proj.root / "assets")
|
||||
manifest.step("render", succes=bool(pptx_path))
|
||||
if pptx_path and pptx_path.exists():
|
||||
manifest.file(pptx_path)
|
||||
section("PRÉSENTATION GÉNÉRÉE")
|
||||
ok(f"Fichier PPTX : {pptx_path}")
|
||||
ok(f"Taille : {pptx_path.stat().st_size/1024:.1f} Ko")
|
||||
maybe_preview(pptx_path)
|
||||
else:
|
||||
warn("Le PPTX n'a pas pu être généré.")
|
||||
info(f"Le YAML est dans : {proj.outputs}")
|
||||
@@ -1499,6 +1721,15 @@ def run_reprise(proj, layouts):
|
||||
reprime_narrator(narrator, proj) # réamorçage avec contexte
|
||||
proj.log("Reprise du projet pour révision.")
|
||||
markdown = state.get("dernier_markdown", "")
|
||||
# On est déjà en révision (choix explicite ci-dessus) : on lance
|
||||
# directement la session de travail, sans repasser par un menu
|
||||
# "veux-tu réviser ?" redondant.
|
||||
markdown = run_revision(narrator, markdown, layouts, proj, manifest)
|
||||
manifest.save(
|
||||
proj.outputs /
|
||||
f"{proj.slug}_{datetime.now():%Y%m%d_%H%M%S}_manifest.json")
|
||||
# Boucle de suite : une fois cette révision faite, proposer d'itérer
|
||||
# encore ou de terminer — ce menu-ci n'est pas redondant.
|
||||
revision_loop(narrator, markdown, layouts, proj, manifest)
|
||||
|
||||
elif choix == "2":
|
||||
@@ -1570,8 +1801,17 @@ def main():
|
||||
description="Sliding facilitator v8 — flux libre (Free Designer)")
|
||||
parser.add_argument("--render", metavar="YAML",
|
||||
help="Rendu direct d'un YAML existant, sans agents")
|
||||
parser.add_argument("--preview", metavar="PPTX",
|
||||
help="Aperçus PNG d'un PPTX existant (C2)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.preview:
|
||||
p = Path(args.preview)
|
||||
if not p.exists():
|
||||
warn(f"Fichier introuvable : {p}")
|
||||
sys.exit(1)
|
||||
sys.exit(0 if run_preview(p, wait=True) else 1)
|
||||
|
||||
if args.render:
|
||||
yaml_path = Path(args.render)
|
||||
if not yaml_path.exists():
|
||||
@@ -1581,7 +1821,9 @@ def main():
|
||||
is_valid, message, _ = validate_yaml(
|
||||
yaml_path.read_text(encoding="utf-8"), layouts)
|
||||
(ok if is_valid else warn)(f"Validation : {message}")
|
||||
sys.exit(0 if run_render(yaml_path) else 1)
|
||||
local_assets = yaml_path.parent / "assets"
|
||||
assets_dir = local_assets if local_assets.is_dir() else None
|
||||
sys.exit(0 if run_render(yaml_path, assets_dir) else 1)
|
||||
|
||||
run_pipeline()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user