griptape-nodes 0.60.4__py3-none-any.whl → 0.62.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +0 -1
  2. griptape_nodes/common/macro_parser/__init__.py +16 -1
  3. griptape_nodes/common/macro_parser/core.py +19 -7
  4. griptape_nodes/common/macro_parser/exceptions.py +99 -0
  5. griptape_nodes/common/macro_parser/formats.py +13 -4
  6. griptape_nodes/common/macro_parser/matching.py +5 -2
  7. griptape_nodes/common/macro_parser/parsing.py +48 -8
  8. griptape_nodes/common/macro_parser/resolution.py +23 -5
  9. griptape_nodes/common/project_templates/__init__.py +49 -0
  10. griptape_nodes/common/project_templates/default_project_template.py +87 -0
  11. griptape_nodes/common/project_templates/defaults/README.md +36 -0
  12. griptape_nodes/common/project_templates/directory.py +67 -0
  13. griptape_nodes/common/project_templates/loader.py +342 -0
  14. griptape_nodes/common/project_templates/project.py +252 -0
  15. griptape_nodes/common/project_templates/situation.py +143 -0
  16. griptape_nodes/common/project_templates/validation.py +140 -0
  17. griptape_nodes/exe_types/core_types.py +36 -3
  18. griptape_nodes/exe_types/node_types.py +4 -2
  19. griptape_nodes/exe_types/param_components/progress_bar_component.py +57 -0
  20. griptape_nodes/exe_types/param_types/parameter_audio.py +243 -0
  21. griptape_nodes/exe_types/param_types/parameter_image.py +243 -0
  22. griptape_nodes/exe_types/param_types/parameter_three_d.py +215 -0
  23. griptape_nodes/exe_types/param_types/parameter_video.py +243 -0
  24. griptape_nodes/node_library/workflow_registry.py +1 -1
  25. griptape_nodes/retained_mode/events/execution_events.py +41 -0
  26. griptape_nodes/retained_mode/events/node_events.py +90 -1
  27. griptape_nodes/retained_mode/events/os_events.py +108 -0
  28. griptape_nodes/retained_mode/events/parameter_events.py +1 -1
  29. griptape_nodes/retained_mode/events/project_events.py +528 -0
  30. griptape_nodes/retained_mode/events/workflow_events.py +19 -1
  31. griptape_nodes/retained_mode/griptape_nodes.py +9 -1
  32. griptape_nodes/retained_mode/managers/agent_manager.py +18 -24
  33. griptape_nodes/retained_mode/managers/event_manager.py +6 -9
  34. griptape_nodes/retained_mode/managers/flow_manager.py +63 -0
  35. griptape_nodes/retained_mode/managers/library_manager.py +55 -42
  36. griptape_nodes/retained_mode/managers/mcp_manager.py +14 -6
  37. griptape_nodes/retained_mode/managers/node_manager.py +232 -0
  38. griptape_nodes/retained_mode/managers/os_manager.py +399 -6
  39. griptape_nodes/retained_mode/managers/project_manager.py +1067 -0
  40. griptape_nodes/retained_mode/managers/settings.py +6 -0
  41. griptape_nodes/retained_mode/managers/sync_manager.py +4 -1
  42. griptape_nodes/retained_mode/managers/workflow_manager.py +8 -79
  43. griptape_nodes/traits/button.py +19 -0
  44. {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.62.0.dist-info}/METADATA +5 -3
  45. {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.62.0.dist-info}/RECORD +47 -32
  46. {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.62.0.dist-info}/WHEEL +1 -1
  47. {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.62.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,143 @@
1
+ """Situation template definitions for file path scenarios."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import StrEnum
6
+ from typing import TYPE_CHECKING, Any, ClassVar
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
9
+
10
+ from griptape_nodes.common.macro_parser import MacroSyntaxError, ParsedMacro
11
+
12
+ if TYPE_CHECKING:
13
+ from griptape_nodes.common.project_templates.loader import YAMLLineInfo
14
+ from griptape_nodes.common.project_templates.validation import ProjectValidationInfo
15
+
16
+
17
+ class SituationFilePolicy(StrEnum):
18
+ """Policy for handling file collisions in situations.
19
+
20
+ Maps to ExistingFilePolicy for file operations, except PROMPT which
21
+ triggers user interaction before determining final policy.
22
+ """
23
+
24
+ CREATE_NEW = "create_new" # Increment {_index} in macro
25
+ OVERWRITE = "overwrite" # Maps to ExistingFilePolicy.OVERWRITE
26
+ FAIL = "fail" # Maps to ExistingFilePolicy.FAIL
27
+ PROMPT = "prompt" # Special UI handling
28
+
29
+
30
+ class SituationPolicy(BaseModel):
31
+ """Policy for file operations in a situation."""
32
+
33
+ on_collision: SituationFilePolicy = Field(description="Policy for handling file collisions")
34
+ create_dirs: bool = Field(description="Whether to create directories automatically")
35
+
36
+
37
+ class SituationTemplate(BaseModel):
38
+ """Template defining how files are saved in a specific situation."""
39
+
40
+ LATEST_SCHEMA_VERSION: ClassVar[str] = "0.1.0"
41
+
42
+ model_config = ConfigDict(populate_by_name=True)
43
+
44
+ name: str = Field(description="Name of the situation")
45
+ macro: str = Field(description="Macro template for file path")
46
+ policy: SituationPolicy = Field(description="Policy for file operations")
47
+ fallback: str | None = Field(default=None, description="Name of fallback situation")
48
+ description: str | None = Field(default=None, description="Description of the situation")
49
+
50
+ @field_validator("macro")
51
+ @classmethod
52
+ def validate_macro_syntax(cls, v: str) -> str:
53
+ """Validate macro syntax using macro parser."""
54
+ try:
55
+ ParsedMacro(v)
56
+ except MacroSyntaxError as e:
57
+ msg = f"Invalid macro syntax: {e}"
58
+ raise ValueError(msg) from e
59
+ return v
60
+
61
+ @staticmethod
62
+ def merge(
63
+ base: SituationTemplate,
64
+ overlay_data: dict[str, Any],
65
+ field_path: str,
66
+ validation_info: ProjectValidationInfo,
67
+ line_info: YAMLLineInfo,
68
+ ) -> SituationTemplate:
69
+ """Merge overlay fields onto base situation.
70
+
71
+ Field-level merge behavior:
72
+ - macro: Use overlay if present, else base
73
+ - description: Use overlay if present, else base
74
+ - fallback: Use overlay if present, else base
75
+ - policy: Use overlay if present (must be complete), else base
76
+
77
+ Policy validation:
78
+ - If policy provided in overlay, must contain both on_collision and create_dirs
79
+ - Adds error to validation_info if incomplete
80
+
81
+ Args:
82
+ base: Complete base situation to start from
83
+ overlay_data: Partial situation dict from overlay YAML
84
+ field_path: Path for validation errors (e.g., "situations.save_file")
85
+ validation_info: Shared validation info
86
+ line_info: Line tracking from overlay YAML
87
+
88
+ Returns:
89
+ New merged SituationTemplate
90
+ """
91
+ # Start with base fields as dict
92
+ merged_data = base.model_dump()
93
+
94
+ # Apply overlay fields if present
95
+ if "macro" in overlay_data:
96
+ merged_data["macro"] = overlay_data["macro"]
97
+
98
+ if "description" in overlay_data:
99
+ merged_data["description"] = overlay_data["description"]
100
+
101
+ if "fallback" in overlay_data:
102
+ merged_data["fallback"] = overlay_data["fallback"]
103
+
104
+ # Policy must be complete if provided
105
+ if "policy" in overlay_data:
106
+ policy = overlay_data["policy"]
107
+ if not isinstance(policy, dict):
108
+ validation_info.add_error(
109
+ field_path=f"{field_path}.policy",
110
+ message="Policy must be a dict",
111
+ line_number=line_info.get_line(f"{field_path}.policy"),
112
+ )
113
+ elif "on_collision" not in policy or "create_dirs" not in policy:
114
+ validation_info.add_error(
115
+ field_path=f"{field_path}.policy",
116
+ message="Policy must include both on_collision and create_dirs",
117
+ line_number=line_info.get_line(f"{field_path}.policy"),
118
+ )
119
+ else:
120
+ merged_data["policy"] = policy
121
+
122
+ # Build merged situation using model_validate
123
+ # Note: name field is not in overlay_data, use base.name
124
+ merged_data_with_name = {"name": base.name, **merged_data}
125
+
126
+ try:
127
+ return SituationTemplate.model_validate(merged_data_with_name)
128
+ except ValidationError as e:
129
+ # Convert Pydantic validation errors to our validation_info format
130
+ for error in e.errors():
131
+ error_field_path = ".".join(str(loc) for loc in error["loc"])
132
+ full_field_path = f"{field_path}.{error_field_path}"
133
+ message = error["msg"]
134
+ line_number = line_info.get_line(full_field_path)
135
+
136
+ validation_info.add_error(
137
+ field_path=full_field_path,
138
+ message=message,
139
+ line_number=line_number,
140
+ )
141
+
142
+ # Return base on validation error (fault-tolerant)
143
+ return base
@@ -0,0 +1,140 @@
1
+ """Validation infrastructure for project templates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import StrEnum
7
+
8
+
9
+ class ProjectValidationStatus(StrEnum):
10
+ """Status of project template validation.
11
+
12
+ Mirrors WorkflowStatus pattern from workflow_manager.py.
13
+ """
14
+
15
+ GOOD = "GOOD" # No errors detected
16
+ FLAWED = "FLAWED" # Some errors, but recoverable
17
+ UNUSABLE = "UNUSABLE" # Errors make template unusable
18
+ MISSING = "MISSING" # File not found
19
+
20
+
21
+ class ProjectValidationProblemSeverity(StrEnum):
22
+ """Severity level of a validation problem."""
23
+
24
+ ERROR = "error"
25
+ WARNING = "warning"
26
+
27
+
28
+ class ProjectOverrideCategory(StrEnum):
29
+ """Category of template override during merge."""
30
+
31
+ SITUATION = "situation"
32
+ DIRECTORY = "directory"
33
+ ENVIRONMENT = "environment"
34
+ METADATA = "metadata"
35
+
36
+
37
+ class ProjectOverrideAction(StrEnum):
38
+ """Action taken during template merge."""
39
+
40
+ MODIFIED = "modified" # Existed in base, changed in overlay
41
+ ADDED = "added" # New in overlay, not in base
42
+
43
+
44
+ @dataclass
45
+ class ProjectValidationProblem:
46
+ """Single validation problem with context."""
47
+
48
+ line_number: int | None
49
+ field_path: str # e.g., "situations.copy_external_file.macro"
50
+ message: str
51
+ severity: ProjectValidationProblemSeverity
52
+
53
+
54
+ @dataclass
55
+ class ProjectOverride:
56
+ """Record of a template override during merge.
57
+
58
+ Tracks what was customized in the overlay without affecting validation status.
59
+ """
60
+
61
+ category: ProjectOverrideCategory
62
+ name: str
63
+ action: ProjectOverrideAction
64
+
65
+
66
+ @dataclass
67
+ class ProjectValidationInfo:
68
+ """Validation result for a project template.
69
+
70
+ Shared across construction chain - problems are accumulated as
71
+ the template is built from YAML.
72
+
73
+ Overrides track what was customized during merge without affecting status.
74
+ """
75
+
76
+ status: ProjectValidationStatus
77
+ problems: list[ProjectValidationProblem] = field(default_factory=list)
78
+ overrides: list[ProjectOverride] = field(default_factory=list)
79
+
80
+ def add_error(self, field_path: str, message: str, line_number: int | None = None) -> None:
81
+ """Add an error to the problems list.
82
+
83
+ Automatically downgrades status to UNUSABLE unless already MISSING.
84
+ Early returns if status is MISSING.
85
+ """
86
+ if self.status == ProjectValidationStatus.MISSING:
87
+ return
88
+
89
+ self.problems.append(
90
+ ProjectValidationProblem(
91
+ line_number=line_number,
92
+ field_path=field_path,
93
+ message=message,
94
+ severity=ProjectValidationProblemSeverity.ERROR,
95
+ ),
96
+ )
97
+ self.status = ProjectValidationStatus.UNUSABLE
98
+
99
+ def add_warning(self, field_path: str, message: str, line_number: int | None = None) -> None:
100
+ """Add a warning to the problems list.
101
+
102
+ Automatically downgrades status from GOOD to FLAWED if current status is GOOD.
103
+ Does not change status if already FLAWED or UNUSABLE.
104
+ Early returns if status is MISSING.
105
+ """
106
+ if self.status == ProjectValidationStatus.MISSING:
107
+ return
108
+
109
+ self.problems.append(
110
+ ProjectValidationProblem(
111
+ line_number=line_number,
112
+ field_path=field_path,
113
+ message=message,
114
+ severity=ProjectValidationProblemSeverity.WARNING,
115
+ ),
116
+ )
117
+
118
+ if self.status == ProjectValidationStatus.GOOD:
119
+ self.status = ProjectValidationStatus.FLAWED
120
+
121
+ def add_override(
122
+ self,
123
+ category: ProjectOverrideCategory,
124
+ name: str,
125
+ action: ProjectOverrideAction,
126
+ ) -> None:
127
+ """Record an override without affecting validation status.
128
+
129
+ Used during template merge to track what was customized in the overlay.
130
+
131
+ Args:
132
+ category: Type of override (situation, directory, environment, metadata)
133
+ name: Name of the item that was overridden
134
+ action: Whether it was modified (existed in base) or added (new in overlay)
135
+ """
136
+ self.overrides.append(ProjectOverride(category=category, name=name, action=action))
137
+
138
+ def is_usable(self) -> bool:
139
+ """Check if template can be used (GOOD or FLAWED status)."""
140
+ return self.status in (ProjectValidationStatus.GOOD, ProjectValidationStatus.FLAWED)
@@ -787,7 +787,7 @@ class DeprecationMessage(ParameterMessage):
787
787
  kwargs.setdefault("traits", {})
788
788
  kwargs["traits"][Button(label=button_text, icon="plus", variant="secondary", on_click=migrate_function)] = None
789
789
 
790
- super().__init__(value=value, **kwargs)
790
+ super().__init__(value=value, button_text=button_text, **kwargs)
791
791
 
792
792
  def to_dict(self) -> dict:
793
793
  """Override to_dict to use element_type instead of class name.
@@ -804,9 +804,18 @@ class DeprecationMessage(ParameterMessage):
804
804
  class ParameterGroup(BaseNodeElement, UIOptionsMixin):
805
805
  """UI element for a group of parameters."""
806
806
 
807
- def __init__(self, name: str, ui_options: dict | None = None, **kwargs):
807
+ def __init__(self, name: str, ui_options: dict | None = None, *, collapsed: bool = False, **kwargs):
808
808
  super().__init__(name=name, **kwargs)
809
- self._ui_options = ui_options or {}
809
+ if ui_options is None:
810
+ ui_options = {}
811
+ else:
812
+ ui_options = ui_options.copy()
813
+
814
+ # Add collapsed to ui_options if it's True
815
+ if collapsed:
816
+ ui_options["collapsed"] = collapsed
817
+
818
+ self._ui_options = ui_options
810
819
 
811
820
  @property
812
821
  def ui_options(self) -> dict:
@@ -817,6 +826,30 @@ class ParameterGroup(BaseNodeElement, UIOptionsMixin):
817
826
  def ui_options(self, value: dict) -> None:
818
827
  self._ui_options = value
819
828
 
829
+ @property
830
+ def collapsed(self) -> bool:
831
+ """Get whether the parameter group is collapsed.
832
+
833
+ Returns:
834
+ True if the group is collapsed, False otherwise
835
+ """
836
+ return self._ui_options.get("collapsed", False)
837
+
838
+ @collapsed.setter
839
+ @BaseNodeElement.emits_update_on_write
840
+ def collapsed(self, value: bool) -> None:
841
+ """Set whether the parameter group is collapsed.
842
+
843
+ Args:
844
+ value: Whether to collapse the group
845
+ """
846
+ if value:
847
+ self.update_ui_options_key("collapsed", value)
848
+ else:
849
+ ui_options = self._ui_options.copy()
850
+ ui_options.pop("collapsed", None)
851
+ self._ui_options = ui_options
852
+
820
853
  def to_dict(self) -> dict[str, Any]:
821
854
  """Returns a nested dictionary representation of this node and its children.
822
855
 
@@ -793,13 +793,15 @@ class BaseNode(ABC):
793
793
 
794
794
  def get_parameter_value(self, param_name: str) -> Any:
795
795
  param = self.get_parameter_by_name(param_name)
796
- if param and isinstance(param, ParameterContainer):
796
+ if param is None:
797
+ return None
798
+ if isinstance(param, ParameterContainer):
797
799
  value = handle_container_parameter(self, param)
798
800
  if value is not None:
799
801
  return value
800
802
  if param_name in self.parameter_values:
801
803
  return self.parameter_values[param_name]
802
- return param.default_value if param else None
804
+ return param.default_value
803
805
 
804
806
  def get_parameter_list_value(self, param: str) -> list:
805
807
  """Flattens the given param from self.params into a single list.
@@ -0,0 +1,57 @@
1
+ import logging
2
+
3
+ from griptape_nodes.exe_types.core_types import Parameter, ParameterMode
4
+ from griptape_nodes.exe_types.node_types import BaseNode
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class ProgressBarComponent:
10
+ def __init__(self, node: BaseNode):
11
+ self._node = node
12
+ self._total_steps = 0
13
+ self._current_step = 0
14
+
15
+ def add_property_parameters(self) -> None:
16
+ self._node.add_parameter(
17
+ Parameter(
18
+ name="progress",
19
+ output_type="float",
20
+ allowed_modes={ParameterMode.PROPERTY},
21
+ tooltip="Progress bar showing completion (0.0 to 1.0)",
22
+ ui_options={"progress_bar": True},
23
+ settable=False,
24
+ )
25
+ )
26
+
27
+ def initialize(self, total_steps: int) -> None:
28
+ """Initialize the progress bar with a total number of steps."""
29
+ self._total_steps = total_steps
30
+ self._current_step = 0
31
+ self._update_progress()
32
+
33
+ def increment(self, steps: int = 1) -> None:
34
+ """Increment the progress by the specified number of steps."""
35
+ self._current_step += steps
36
+ if self._current_step > self._total_steps:
37
+ logger.warning(
38
+ "Current step %i exceeds total steps %i. Progress will not exceed 100%.",
39
+ self._current_step,
40
+ self._total_steps,
41
+ )
42
+ self._update_progress()
43
+
44
+ def reset(self) -> None:
45
+ """Reset the progress bar to 0."""
46
+ self._current_step = 0
47
+ self._total_steps = 0
48
+ self._update_progress()
49
+
50
+ def _update_progress(self) -> None:
51
+ """Update the progress parameter based on current step and total steps."""
52
+ if self._total_steps <= 0:
53
+ progress_value = 0.0
54
+ else:
55
+ progress_value = min(1.0, self._current_step / self._total_steps)
56
+
57
+ self._node.publish_update_to_parameter("progress", progress_value)
@@ -0,0 +1,243 @@
1
+ """ParameterAudio component for audio inputs with enhanced UI options."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from griptape_nodes.exe_types.core_types import Parameter, ParameterMode, Trait
7
+
8
+
9
+ class ParameterAudio(Parameter):
10
+ """A specialized Parameter class for audio inputs with enhanced UI options.
11
+
12
+ This class provides a convenient way to create audio parameters with common
13
+ UI customizations like file browser, microphone capture, and audio editing.
14
+ It exposes these UI options as direct properties for easy runtime modification.
15
+
16
+ Example:
17
+ param = ParameterAudio(
18
+ name="input_audio",
19
+ tooltip="Select an audio file",
20
+ clickable_file_browser=True,
21
+ microphone_capture_audio=True,
22
+ edit_audio=True
23
+ )
24
+ param.pulse_on_run = True # Change UI options at runtime
25
+ """
26
+
27
+ def __init__( # noqa: PLR0913
28
+ self,
29
+ name: str,
30
+ tooltip: str | None = None,
31
+ *,
32
+ type: str = "AudioUrlArtifact", # noqa: A002, ARG002
33
+ input_types: list[str] | None = None, # noqa: ARG002
34
+ output_type: str = "AudioUrlArtifact", # noqa: ARG002
35
+ default_value: Any = None,
36
+ tooltip_as_input: str | None = None,
37
+ tooltip_as_property: str | None = None,
38
+ tooltip_as_output: str | None = None,
39
+ allowed_modes: set[ParameterMode] | None = None,
40
+ traits: set[type[Trait] | Trait] | None = None,
41
+ converters: list[Callable[[Any], Any]] | None = None,
42
+ validators: list[Callable[[Parameter, Any], None]] | None = None,
43
+ ui_options: dict | None = None,
44
+ pulse_on_run: bool = False,
45
+ clickable_file_browser: bool = True,
46
+ microphone_capture_audio: bool = False,
47
+ edit_audio: bool = False,
48
+ accept_any: bool = True,
49
+ hide: bool = False,
50
+ hide_label: bool = False,
51
+ hide_property: bool = False,
52
+ allow_input: bool = True,
53
+ allow_property: bool = True,
54
+ allow_output: bool = True,
55
+ settable: bool = True,
56
+ serializable: bool = True,
57
+ user_defined: bool = False,
58
+ element_id: str | None = None,
59
+ element_type: str | None = None,
60
+ parent_container_name: str | None = None,
61
+ ) -> None:
62
+ """Initialize an audio parameter with enhanced UI options.
63
+
64
+ Args:
65
+ name: Parameter name
66
+ tooltip: Parameter tooltip
67
+ type: Parameter type (ignored, always "AudioUrlArtifact" for ParameterAudio)
68
+ input_types: Allowed input types (ignored, set based on accept_any)
69
+ output_type: Output type (ignored, always "AudioUrlArtifact" for ParameterAudio)
70
+ default_value: Default parameter value
71
+ tooltip_as_input: Tooltip for input mode
72
+ tooltip_as_property: Tooltip for property mode
73
+ tooltip_as_output: Tooltip for output mode
74
+ allowed_modes: Allowed parameter modes
75
+ traits: Parameter traits
76
+ converters: Parameter converters
77
+ validators: Parameter validators
78
+ ui_options: Dictionary of UI options
79
+ pulse_on_run: Whether to pulse the parameter on run
80
+ clickable_file_browser: Whether to show clickable file browser
81
+ microphone_capture_audio: Whether to enable microphone capture
82
+ edit_audio: Whether to enable audio editing functionality
83
+ accept_any: Whether to accept any input type and convert to audio (default: True)
84
+ hide: Whether to hide the entire parameter
85
+ hide_label: Whether to hide the parameter label
86
+ hide_property: Whether to hide the parameter in property mode
87
+ allow_input: Whether to allow input mode
88
+ allow_property: Whether to allow property mode
89
+ allow_output: Whether to allow output mode
90
+ settable: Whether the parameter is settable
91
+ serializable: Whether the parameter is serializable
92
+ user_defined: Whether the parameter is user-defined
93
+ element_id: Element ID
94
+ element_type: Element type
95
+ parent_container_name: Name of parent container
96
+ """
97
+ # Build ui_options dictionary from the provided UI-specific parameters
98
+ if ui_options is None:
99
+ ui_options = {}
100
+ else:
101
+ ui_options = ui_options.copy()
102
+
103
+ # Add audio-specific UI options if they have values
104
+ if pulse_on_run:
105
+ ui_options["pulse_on_run"] = pulse_on_run
106
+ if clickable_file_browser:
107
+ ui_options["clickable_file_browser"] = clickable_file_browser
108
+ if microphone_capture_audio:
109
+ ui_options["microphone_capture_audio"] = microphone_capture_audio
110
+ if edit_audio:
111
+ ui_options["edit_audio"] = edit_audio
112
+
113
+ # Auto-disable clickable_file_browser if neither input nor property modes are allowed
114
+ if not allow_input and not allow_property and clickable_file_browser:
115
+ ui_options.pop("clickable_file_browser", None)
116
+
117
+ # Set up input types based on accept_any setting
118
+ if accept_any:
119
+ final_input_types = ["any"]
120
+ else:
121
+ final_input_types = ["AudioUrlArtifact"]
122
+
123
+ # Call parent with explicit parameters, following ControlParameter pattern
124
+ super().__init__(
125
+ name=name,
126
+ tooltip=tooltip,
127
+ type="AudioUrlArtifact", # Always an AudioUrlArtifact type for ParameterAudio
128
+ input_types=final_input_types,
129
+ output_type="AudioUrlArtifact", # Always output as AudioUrlArtifact
130
+ default_value=default_value,
131
+ tooltip_as_input=tooltip_as_input,
132
+ tooltip_as_property=tooltip_as_property,
133
+ tooltip_as_output=tooltip_as_output,
134
+ allowed_modes=allowed_modes,
135
+ traits=traits,
136
+ converters=converters,
137
+ validators=validators,
138
+ ui_options=ui_options,
139
+ hide=hide,
140
+ hide_label=hide_label,
141
+ hide_property=hide_property,
142
+ allow_input=allow_input,
143
+ allow_property=allow_property,
144
+ allow_output=allow_output,
145
+ settable=settable,
146
+ serializable=serializable,
147
+ user_defined=user_defined,
148
+ element_id=element_id,
149
+ element_type=element_type,
150
+ parent_container_name=parent_container_name,
151
+ )
152
+
153
+ @property
154
+ def pulse_on_run(self) -> bool:
155
+ """Get whether pulse on run is enabled.
156
+
157
+ Returns:
158
+ True if pulse on run is enabled, False otherwise
159
+ """
160
+ return self.ui_options.get("pulse_on_run", False)
161
+
162
+ @pulse_on_run.setter
163
+ def pulse_on_run(self, value: bool) -> None:
164
+ """Set whether pulse on run is enabled.
165
+
166
+ Args:
167
+ value: Whether to enable pulse on run
168
+ """
169
+ if value:
170
+ self.update_ui_options_key("pulse_on_run", value)
171
+ else:
172
+ ui_options = self.ui_options.copy()
173
+ ui_options.pop("pulse_on_run", None)
174
+ self.ui_options = ui_options
175
+
176
+ @property
177
+ def clickable_file_browser(self) -> bool:
178
+ """Get whether clickable file browser is enabled.
179
+
180
+ Returns:
181
+ True if clickable file browser is enabled, False otherwise
182
+ """
183
+ return self.ui_options.get("clickable_file_browser", False)
184
+
185
+ @clickable_file_browser.setter
186
+ def clickable_file_browser(self, value: bool) -> None:
187
+ """Set whether clickable file browser is enabled.
188
+
189
+ Args:
190
+ value: Whether to enable clickable file browser
191
+ """
192
+ if value:
193
+ self.update_ui_options_key("clickable_file_browser", value)
194
+ else:
195
+ ui_options = self.ui_options.copy()
196
+ ui_options.pop("clickable_file_browser", None)
197
+ self.ui_options = ui_options
198
+
199
+ @property
200
+ def microphone_capture_audio(self) -> bool:
201
+ """Get whether microphone capture audio is enabled.
202
+
203
+ Returns:
204
+ True if microphone capture audio is enabled, False otherwise
205
+ """
206
+ return self.ui_options.get("microphone_capture_audio", False)
207
+
208
+ @microphone_capture_audio.setter
209
+ def microphone_capture_audio(self, value: bool) -> None:
210
+ """Set whether microphone capture audio is enabled.
211
+
212
+ Args:
213
+ value: Whether to enable microphone capture audio
214
+ """
215
+ if value:
216
+ self.update_ui_options_key("microphone_capture_audio", value)
217
+ else:
218
+ ui_options = self.ui_options.copy()
219
+ ui_options.pop("microphone_capture_audio", None)
220
+ self.ui_options = ui_options
221
+
222
+ @property
223
+ def edit_audio(self) -> bool:
224
+ """Get whether edit audio is enabled.
225
+
226
+ Returns:
227
+ True if edit audio is enabled, False otherwise
228
+ """
229
+ return self.ui_options.get("edit_audio", False)
230
+
231
+ @edit_audio.setter
232
+ def edit_audio(self, value: bool) -> None:
233
+ """Set whether edit audio is enabled.
234
+
235
+ Args:
236
+ value: Whether to enable edit audio
237
+ """
238
+ if value:
239
+ self.update_ui_options_key("edit_audio", value)
240
+ else:
241
+ ui_options = self.ui_options.copy()
242
+ ui_options.pop("edit_audio", None)
243
+ self.ui_options = ui_options