跳转至

项目与文件路由系统集成 (Working with the Project System)

项目系统 (Project System) 是 Griptape Nodes 的工业级集中式文件存储与路由框架,负责跨所有工作流统筹管理文件的归档组织、模板命名与物理存盘。它彻底终结了代码中硬编码文件路径的不良实践,提供了全工作流高度一致、用户可深度配置的文件 I/O 管道。本篇文档专门面向算子开发者:指导算子如何通过项目系统优雅存盘文件资产。


核心设计概念 (Concepts)

关于项目系统的全局宏观设计——工作区 (Workspace)、项目模板 (Project Templates)、情境 (Situations)、宏路径 (Macros)、逻辑目录 (Directories) 以及环境变量,已在 项目系统用户指南 中系统详述:

面向算子开发者的极简心智模型: 情境 (Situation) 声明了存盘的具体业务场景语义(例如 save_node_output),其绑定的 宏 (Macro) 模板(例如 {outputs}/{node_name?:_}{file_name_base}{_index?:03}.{file_extension})动态计算出物理文件路径,而最终用户可以在完全不改动你一行算子代码的前提下自由魔改所有存盘拓扑。


算子内部集成项目系统的两大范式 (Using Project System in Nodes)

在算子中写入项目文件主要有两种标准范式:


范式 1:ProjectFileParameter(推荐用于算子的输出端口存盘)

当你的算子生成了需要落盘的资产,且希望在前端界面向用户暴露可自定义的文件名配置框时,请始终采用 ProjectFileParameter:

from griptape_nodes.exe_types.param_components.project_file_parameter import ProjectFileParameter
from griptape_nodes.exe_types.core_types import Parameter, ParameterMode
from griptape.artifacts.video_url_artifact import VideoUrlArtifact


class MyVideoNode(ControlNode):
    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)

        # 1. 添加标准的输出参数端口
        self.add_parameter(
            Parameter(
                name="output_video",
                output_type="VideoUrlArtifact",
                tooltip="生成的视频资产",
                allowed_modes={ParameterMode.OUTPUT},
            )
        )

        # 2. 注入 ProjectFileParameter 组件管理存盘参数
        # `situation` 明确声明了该算子依据哪种业务情境存盘 (默认值为 "save_node_output")
        # 建议显式传入以便代码审计与他人协作
        self._output_video_file = ProjectFileParameter(
            node=self,
            name="output_video_file",
            default_filename="output_video.mp4",
            situation="save_node_output",
        )
        self._output_video_file.add_parameter()

    def process(self) -> None:
        # ... 业务计算逻辑生成 video_bytes 字节流 ...

        # 3. 调用 build_file() 构造 ProjectFileDestination 目标句柄
        dest = self._output_video_file.build_file()
        saved = dest.write_bytes(video_bytes)

        # 4. 将物理落地后的实际访问 URL 赋给输出端口
        self.parameter_output_values["output_video"] = VideoUrlArtifact(saved.location)

核心准则:

  • ProjectFileParameter 会在节点面板上自动注册一个可供用户编辑的基础输入参数;
  • 调用 build_file() 获取 ProjectFileDestination 实例;
  • 调用 write_bytes() 写入二进制字节流;
  • 始终通过 saved.location 提取最终文件解析完毕的实际路径或网络 URL。

情境 (Situation) 的运作机制:

  • situation 参数是在构造函数中传递的类属性,绝不会作为前端可直接输入修改的文本参数存在;
  • 前端暴露的文本参数(通常默认命名为 output_file)仅承载基础文件名而非完整物理路径。build_file() 会自动将用户输入的字符串拆解为 {file_name_base} 与 {file_extension},并代入所属情境的 Macro 模板中。例如用户输入 render.png,在 save_node_output 情境下最终落地路径将自动演进为 outputs/MyNode_render.png;
  • 用户若需对特定节点的情境实施高级覆盖,可点击参数旁的齿轮图标,挂载 FileOutputSettings 节点进行热覆写。

⚠️ 高频致命陷阱:未捕获 write_bytes() 的返回值!

# ❌ 严重错误写法:
dest = self._output_video_file.build_file()
dest.write_bytes(video_bytes)  # ❌ 丢弃了返回值
artifact = VideoUrlArtifact(dest.location)  # 错误地直接读取未计算完毕的 dest

# 这将引发致命报错: "Failed because missing required variables: file_extension, file_name_base"

原因剖析:{file_extension} 与 {file_name_base} 等宏变量是在执行 write_bytes() 真正实施文件落盘时才动态计算并填充进返回的已保存对象中的。在写入前直接访问 dest.location 会因宏变量尚未填充而触发解析异常。

# ✅ 正确规范写法:
dest = self._output_video_file.build_file()
saved = dest.write_bytes(video_bytes)  # ✅ 必须捕获已存盘的落地对象
artifact = VideoUrlArtifact(saved.location)  # 使用解析完全的已保存文件地址

范式 2:直接使用 ProjectFileDestination(专用于内部辅助函数)

在无需向用户开放 UI 输入框的纯底层辅助函数中,可直接调用 ProjectFileDestination.from_situation():

from griptape_nodes.files.project_file import ProjectFileDestination
from griptape.artifacts.video_url_artifact import VideoUrlArtifact


def frames_to_video_artifact(frames: list, fps: int = 30, video_format: str = "mp4") -> VideoUrlArtifact:
    """将帧图像列表编码压缩并写入项目文件系统。"""
    # ... 视频转码得到 video_bytes ...

    # 直接依附指定情境创建写入目标
    dest = ProjectFileDestination.from_situation(filename=f"video.{video_format}", situation="save_node_output")
    saved = dest.write_bytes(video_bytes)

    return VideoUrlArtifact(saved.location)

彻底废弃老旧的 StaticFilesManager (Migration Guide)

历史旧写法 (已全面废弃 ❌)

from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
import uuid


def old_save_video(video_bytes: bytes) -> VideoUrlArtifact:
    filename = f"{uuid.uuid4()}.mp4"
    url = GriptapeNodes.StaticFilesManager().save_static_file(video_bytes, filename)
    return VideoUrlArtifact(url)

现代项目系统标准写法 (✅)

from griptape_nodes.files.project_file import ProjectFileDestination


def new_save_video(video_bytes: bytes) -> VideoUrlArtifact:
    dest = ProjectFileDestination.from_situation(filename="video.mp4", situation="save_node_output")
    saved = dest.write_bytes(video_bytes)
    return VideoUrlArtifact(saved.location)

升级收益:

  • 杜绝在业务代码中手动生成丑陋脆弱的 UUID;
  • 全局所有算子的文件命名与目录结构高度归一统筹;
  • 用户可通过项目配置文件全自动自定义全量输出路径;
  • 底层全自动处理文件名数字编号自增与防覆写冲突。

常用内置情境清单与选型策略 (Common Situations)

  • save_node_output:最为核心的基础情境,专用于节点运算生成的图像、视频、音频成片输出;
  • copy_external_file:将工作区外部的本地物理文件复制导入到项目目录内时选用;
  • download_url:从公网 URL 下载网络资源并本地持久化缓存时选用;
  • save_preview:渲染缩略图、预览快照或海报帧专用;
  • save_static_file:在多轮工作流运行之间保持完全不变的静态资源资产。

完整的情境列表、宏模板与重名冲突策略请参阅 内置情境速查表。


生产级最佳实践 (Best Practices)

  1. 绝对不要在算子中硬编码物理路径:所有文件持久化操作必须委托给项目系统;
  2. 选择恰当的接入范式:面向用户需自定义命名的情况使用 ProjectFileParameter;纯底层工具函数使用 ProjectFileDestination;
  3. 精准声明语义化情境:如实选择反映业务特性的 situation;
  4. 让宏系统掌控重名编号:禁止在文件名中手动硬编码时间戳或自增序列,重名自增策略完全由宏引擎透明处理;
  5. 严谨管理临时文件:计算中间过程的未完成数据请使用 Python 标准库 tempfile 在系统临时目录处理;唯有最终成片才写入项目系统,并在写入完成后立即释放清理临时文件。

完整实战范例:视频处理算子 (Complete Video Node)

以下是一个融合了临时文件无损转码、异常安全防御以及项目系统标准存盘的生产级视频算子代码模板:

import tempfile
from pathlib import Path
from typing import Any

from griptape.artifacts.video_url_artifact import VideoUrlArtifact
from griptape_nodes.exe_types.core_types import Parameter, ParameterMode
from griptape_nodes.exe_types.node_types import ControlNode, AsyncResult
from griptape_nodes.exe_types.param_components.project_file_parameter import ProjectFileParameter
from griptape_nodes.files.file import File


class ProcessVideo(ControlNode):
    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)

        # 声明输入端口
        self.add_parameter(
            Parameter(
                name="input_video",
                input_types=["VideoUrlArtifact"],
                type="VideoUrlArtifact",
                tooltip="待处理的输入源视频",
            )
        )

        # 声明输出端口
        self.add_parameter(
            Parameter(
                name="output_video",
                output_type="VideoUrlArtifact",
                tooltip="处理后生成的视频输出",
                allowed_modes={ParameterMode.OUTPUT},
            )
        )

        # 挂载项目系统存盘参数组件
        self._output_video_file = ProjectFileParameter(
            node=self,
            name="output_video_file",
            default_filename="processed_video.mp4",
            situation="save_node_output",
        )
        self._output_video_file.add_parameter()

    def process(self) -> AsyncResult:
        # 将耗时转码委托给后台线程池
        yield lambda: self._process()

    def _process(self) -> None:
        # 1. 提取并读取输入视频的二进制字节流
        input_artifact = self.get_parameter_value("input_video")
        input_bytes = File(input_artifact.value).read_bytes()

        # 2. 在系统临时空间建立中转缓存文件
        with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_file:
            temp_path = Path(temp_file.name)

        try:
            # 写入临时中转文件
            temp_path.write_bytes(input_bytes)

            # ... 在本地调用 ffmpeg 或 cv2 对 temp_path 执行实质转码运算 ...

            # 读取最终加工后的成品字节流
            output_bytes = temp_path.read_bytes()

            # 3. 通过项目系统优雅持久化存盘
            dest = self._output_video_file.build_file()
            saved = dest.write_bytes(output_bytes)

            # 4. 提取完全解析的路径并赋给输出端口
            self.parameter_output_values["output_video"] = VideoUrlArtifact(saved.location)

        finally:
            # 5. 必须在 finally 块中确保临时文件无残留销毁清理
            if temp_path.exists():
                temp_path.unlink()