Skip to content

Commit 9dfe9ee

Browse files
collindutterclaude
authored andcommitted
feat: add OpenAI audio transcription proxy node
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 563436f commit 9dfe9ee

3 files changed

Lines changed: 571 additions & 0 deletions

File tree

griptape_nodes_library.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,17 @@
669669
"group": "create"
670670
}
671671
},
672+
{
673+
"class_name": "OpenAiAudioTranscription",
674+
"file_path": "griptape_nodes_library/audio/openai_audio_transcription.py",
675+
"metadata": {
676+
"category": "audio",
677+
"description": "Transcribe audio to text using OpenAI models via Griptape Cloud proxy",
678+
"display_name": "OpenAI Audio Transcription",
679+
"icon": "ear",
680+
"group": "describe"
681+
}
682+
},
672683
{
673684
"class_name": "CombineAudio",
674685
"file_path": "griptape_nodes_library/audio/combine_audio.py",
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from contextlib import suppress
5+
from typing import Any
6+
7+
from griptape.artifacts.audio_url_artifact import AudioUrlArtifact
8+
from griptape_nodes.exe_types.core_types import ParameterGroup, ParameterMode
9+
from griptape_nodes.exe_types.param_types.parameter_audio import ParameterAudio
10+
from griptape_nodes.exe_types.param_types.parameter_dict import ParameterDict
11+
from griptape_nodes.exe_types.param_types.parameter_float import ParameterFloat
12+
from griptape_nodes.exe_types.param_types.parameter_string import ParameterString
13+
from griptape_nodes.files.file import File, FileLoadError
14+
from griptape_nodes.traits.options import Options
15+
from griptape_nodes.traits.slider import Slider
16+
17+
from griptape_nodes_library.griptape_proxy_node import GriptapeProxyNode
18+
19+
logger = logging.getLogger(__name__)
20+
21+
__all__ = ["OpenAiAudioTranscription"]
22+
23+
MODEL_CHOICES = ["gpt-4o-transcribe", "gpt-4o-mini-transcribe", "whisper-1"]
24+
DEFAULT_MODEL = "gpt-4o-mini-transcribe"
25+
26+
MODEL_MAPPING = {
27+
"gpt-4o-transcribe": "gpt-4o-transcribe",
28+
"gpt-4o-mini-transcribe": "gpt-4o-mini-transcribe",
29+
"whisper-1": "whisper-1",
30+
}
31+
32+
RESPONSE_FORMAT_CHOICES = ["json", "verbose_json"]
33+
DEFAULT_RESPONSE_FORMAT = "json"
34+
35+
36+
class OpenAiAudioTranscription(GriptapeProxyNode):
37+
"""Transcribe audio to text using OpenAI models via Griptape Cloud proxy.
38+
39+
Supports GPT-4o transcription models and Whisper for converting speech to text.
40+
41+
Inputs:
42+
- audio: Audio file to transcribe (mp3, mp4, mpeg, mpga, m4a, wav, webm, flac)
43+
- model: Transcription model to use
44+
- language: ISO-639-1 language code to improve accuracy
45+
- prompt: Optional text to guide transcription style
46+
- response_format: Output format (json or verbose_json)
47+
- temperature: Sampling temperature (0 = deterministic)
48+
49+
Outputs:
50+
- generation_id (str): Generation ID from the API
51+
- provider_response (dict): Verbatim response from the proxy
52+
- output (str): Transcribed text from the audio
53+
- words (list): Word-level timing data (only with verbose_json)
54+
- segments (list): Segment-level data with timing (only with verbose_json)
55+
- detected_language (str): Detected language of the audio
56+
- duration (float): Duration of the audio in seconds
57+
"""
58+
59+
def __init__(self, **kwargs: Any) -> None:
60+
super().__init__(**kwargs)
61+
self.category = "API Nodes"
62+
self.description = "Transcribe audio to text using OpenAI models"
63+
64+
# --- INPUT PARAMETERS ---
65+
self.add_parameter(
66+
ParameterAudio(
67+
name="audio",
68+
default_value=None,
69+
tooltip="Audio to transcribe. Supported formats: mp3, mp4, mpeg, mpga, m4a, wav, webm, flac. Max 25MB.",
70+
allowed_modes={ParameterMode.INPUT, ParameterMode.PROPERTY},
71+
ui_options={"clickable_file_browser": True, "expander": True},
72+
)
73+
)
74+
75+
self.add_parameter(
76+
ParameterString(
77+
name="model",
78+
default_value=DEFAULT_MODEL,
79+
tooltip="Transcription model to use",
80+
allowed_modes={ParameterMode.INPUT, ParameterMode.PROPERTY},
81+
traits={Options(choices=MODEL_CHOICES)},
82+
ui_options={"display_name": "Model"},
83+
)
84+
)
85+
86+
self.add_parameter(
87+
ParameterString(
88+
name="language",
89+
default_value=None,
90+
tooltip="ISO-639-1 language code (e.g. en, es, fr). Providing this improves accuracy and speed.",
91+
allowed_modes={ParameterMode.INPUT, ParameterMode.PROPERTY},
92+
placeholder_text="Auto-detect",
93+
ui_options={"display_name": "Language"},
94+
)
95+
)
96+
97+
self.add_parameter(
98+
ParameterString(
99+
name="prompt",
100+
default_value=None,
101+
tooltip="Optional text to guide transcription style or provide context. Should match the audio language.",
102+
allowed_modes={ParameterMode.INPUT, ParameterMode.PROPERTY},
103+
multiline=True,
104+
placeholder_text="Optional context for the transcription...",
105+
ui_options={"display_name": "Prompt"},
106+
)
107+
)
108+
109+
# Advanced parameters
110+
with ParameterGroup(name="Advanced", ui_options={"collapsed": True}) as advanced_group:
111+
ParameterString(
112+
name="response_format",
113+
default_value=DEFAULT_RESPONSE_FORMAT,
114+
tooltip="Output format. Use verbose_json for word/segment timestamps.",
115+
allowed_modes={ParameterMode.INPUT, ParameterMode.PROPERTY},
116+
traits={Options(choices=RESPONSE_FORMAT_CHOICES)},
117+
ui_options={"display_name": "Response Format"},
118+
)
119+
120+
ParameterFloat(
121+
name="temperature",
122+
default_value=0.0,
123+
tooltip="Sampling temperature between 0 and 1. Higher values produce more random output.",
124+
allowed_modes={ParameterMode.INPUT, ParameterMode.PROPERTY},
125+
traits={Slider(min_val=0.0, max_val=1.0)},
126+
ui_options={"display_name": "Temperature"},
127+
)
128+
self.add_node_element(advanced_group)
129+
130+
# --- OUTPUT PARAMETERS ---
131+
self.add_parameter(
132+
ParameterString(
133+
name="generation_id",
134+
tooltip="Generation ID from the API",
135+
allowed_modes={ParameterMode.OUTPUT},
136+
hide_property=True,
137+
hide=True,
138+
)
139+
)
140+
141+
self.add_parameter(
142+
ParameterDict(
143+
name="provider_response",
144+
tooltip="Verbatim response from Griptape model proxy",
145+
allowed_modes={ParameterMode.OUTPUT},
146+
hide_property=True,
147+
hide=True,
148+
)
149+
)
150+
151+
self.add_parameter(
152+
ParameterString(
153+
name="output",
154+
tooltip="Transcribed text from the audio",
155+
default_value=None,
156+
allowed_modes={ParameterMode.OUTPUT, ParameterMode.PROPERTY},
157+
settable=False,
158+
multiline=True,
159+
placeholder_text="Transcribed text will appear here...",
160+
ui_options={"display_name": "Output", "pulse_on_run": True},
161+
)
162+
)
163+
164+
self.add_parameter(
165+
ParameterDict(
166+
name="words",
167+
tooltip="Word-level timing data (only with verbose_json response format)",
168+
default_value=None,
169+
allowed_modes={ParameterMode.OUTPUT},
170+
hide_property=True,
171+
hide=True,
172+
)
173+
)
174+
175+
self.add_parameter(
176+
ParameterDict(
177+
name="segments",
178+
tooltip="Segment-level data with timing (only with verbose_json response format)",
179+
default_value=None,
180+
allowed_modes={ParameterMode.OUTPUT},
181+
hide_property=True,
182+
hide=True,
183+
)
184+
)
185+
186+
self.add_parameter(
187+
ParameterString(
188+
name="detected_language",
189+
tooltip="Detected language of the audio",
190+
default_value=None,
191+
allowed_modes={ParameterMode.OUTPUT},
192+
hide_property=True,
193+
hide=True,
194+
)
195+
)
196+
197+
self.add_parameter(
198+
ParameterFloat(
199+
name="duration",
200+
tooltip="Duration of the audio in seconds",
201+
default_value=None,
202+
allowed_modes={ParameterMode.OUTPUT},
203+
hide_property=True,
204+
hide=True,
205+
)
206+
)
207+
208+
# Status parameters MUST be last
209+
self._create_status_parameters(
210+
result_details_tooltip="Details about the transcription result or any errors",
211+
result_details_placeholder="Transcription status will appear here...",
212+
parameter_group_initially_collapsed=True,
213+
)
214+
215+
def _get_api_model_id(self) -> str:
216+
"""Get the API model ID for this generation."""
217+
model = self.get_parameter_value("model") or DEFAULT_MODEL
218+
return MODEL_MAPPING.get(str(model), str(model))
219+
220+
def _log(self, message: str) -> None:
221+
with suppress(Exception):
222+
logger.info(message)
223+
224+
async def _resolve_audio_data_uri(self, audio_value: Any) -> str | None:
225+
"""Resolve an audio parameter value to a base64 data URI.
226+
227+
Args:
228+
audio_value: The value from the audio parameter (AudioUrlArtifact, dict, or string)
229+
230+
Returns:
231+
str | None: The base64 data URI, or None if resolution failed
232+
"""
233+
if audio_value is None:
234+
return None
235+
236+
# Handle AudioUrlArtifact
237+
if isinstance(audio_value, AudioUrlArtifact):
238+
audio_url = audio_value.value
239+
elif isinstance(audio_value, dict):
240+
audio_url = audio_value.get("value") or audio_value.get("url", "")
241+
elif isinstance(audio_value, str):
242+
audio_url = audio_value
243+
else:
244+
return None
245+
246+
if not audio_url:
247+
return None
248+
249+
try:
250+
return await File(audio_url).aread_data_uri(fallback_mime="audio/mpeg")
251+
except FileLoadError as e:
252+
self._log(f"Failed to load audio: {e}")
253+
return None
254+
255+
async def _build_payload(self) -> dict[str, Any]:
256+
"""Build the request payload for OpenAI audio transcription."""
257+
audio_value = self.get_parameter_value("audio")
258+
if audio_value is None:
259+
msg = "No audio provided. Please connect an audio source."
260+
raise ValueError(msg)
261+
262+
audio_data_uri = await self._resolve_audio_data_uri(audio_value)
263+
if not audio_data_uri:
264+
msg = "Failed to load audio file. Please check the audio input."
265+
raise ValueError(msg)
266+
267+
language = self.get_parameter_value("language")
268+
prompt = self.get_parameter_value("prompt")
269+
response_format = self.get_parameter_value("response_format") or DEFAULT_RESPONSE_FORMAT
270+
temperature = self.get_parameter_value("temperature")
271+
272+
payload: dict[str, Any] = {
273+
"file": audio_data_uri,
274+
"response_format": response_format,
275+
}
276+
277+
if language:
278+
payload["language"] = language
279+
280+
if prompt:
281+
payload["prompt"] = prompt
282+
283+
if temperature is not None and temperature != 0.0:
284+
payload["temperature"] = temperature
285+
286+
if response_format == "verbose_json":
287+
payload["timestamp_granularities"] = ["word", "segment"]
288+
289+
self._log("Built transcription payload")
290+
return payload
291+
292+
async def _parse_result(self, result_json: dict[str, Any], generation_id: str) -> None:
293+
"""Parse the OpenAI transcription result and set output parameters."""
294+
text = result_json.get("text")
295+
if text is None:
296+
self._set_safe_defaults()
297+
self._set_status_results(
298+
was_successful=False,
299+
result_details="Transcription completed but no text was found in the response.",
300+
)
301+
return
302+
303+
self.parameter_output_values["output"] = text
304+
305+
# Verbose JSON fields
306+
words = result_json.get("words")
307+
if words is not None:
308+
self.parameter_output_values["words"] = words
309+
310+
segments = result_json.get("segments")
311+
if segments is not None:
312+
self.parameter_output_values["segments"] = segments
313+
314+
detected_language = result_json.get("language")
315+
if detected_language is not None:
316+
self.parameter_output_values["detected_language"] = detected_language
317+
318+
duration = result_json.get("duration")
319+
if duration is not None:
320+
self.parameter_output_values["duration"] = duration
321+
322+
self._set_status_results(was_successful=True, result_details="Transcription completed successfully.")
323+
324+
def _set_safe_defaults(self) -> None:
325+
"""Set safe default values for outputs."""
326+
self.parameter_output_values["generation_id"] = ""
327+
self.parameter_output_values["provider_response"] = None
328+
self.parameter_output_values["output"] = None
329+
self.parameter_output_values["words"] = None
330+
self.parameter_output_values["segments"] = None
331+
self.parameter_output_values["detected_language"] = None
332+
self.parameter_output_values["duration"] = None

0 commit comments

Comments
 (0)