Skip to content

Instantly share code, notes, and snippets.

@yuygfgg
Last active April 10, 2026 09:45
Show Gist options
  • Select an option

  • Save yuygfgg/8a2686b230b2919b1d59f88addd4b0f6 to your computer and use it in GitHub Desktop.

Select an option

Save yuygfgg/8a2686b230b2919b1d59f88addd4b0f6 to your computer and use it in GitHub Desktop.
atmos_tools
import argparse
import pathlib
import dataclasses
import subprocess
import shutil
import platform
import os
import sys
SCRIPT_NAME = "Atmos Audio Decoder"
SCRIPT_VERSION = "0.0.1"
if platform.system() != "Darwin":
raise RuntimeError("Error: This script is designed to run on macOS only.")
_NAMES_2_0 = ["L", "R"]
_NAMES_3_1 = ["L", "R", "C", "LFE"]
_NAMES_5_1 = ["L", "R", "C", "LFE", "Ls", "Rs"]
_NAMES_7_1 = _NAMES_5_1 + ["Lrs", "Rrs"]
_NAMES_9_1 = _NAMES_7_1 + ["Lw", "Rw"]
_HEIGHT_NAMES_2 = ["Ltm", "Rtm"]
_HEIGHT_NAMES_4 = ["Ltf", "Rtf", "Ltr", "Rtr"]
_HEIGHT_NAMES_6 = ["Ltf", "Rtf", "Ltm", "Rtm", "Ltr", "Rtr"]
CHANNELS = {
"2.0": {"id": 0, "names": _NAMES_2_0},
"3.1": {"id": 3, "names": _NAMES_3_1},
"5.1": {"id": 7, "names": _NAMES_5_1},
"7.1": {"id": 11, "names": _NAMES_7_1},
"9.1": {"id": 12, "names": _NAMES_9_1},
"5.1.2": {"id": 13, "names": _NAMES_5_1 + _HEIGHT_NAMES_2},
"5.1.4": {"id": 14, "names": _NAMES_5_1 + _HEIGHT_NAMES_4},
"7.1.2": {"id": 15, "names": _NAMES_7_1 + _HEIGHT_NAMES_2},
"7.1.4": {"id": 16, "names": _NAMES_7_1 + _HEIGHT_NAMES_4},
"7.1.6": {"id": 17, "names": _NAMES_7_1 + _HEIGHT_NAMES_6},
"9.1.2": {"id": 18, "names": _NAMES_9_1 + _HEIGHT_NAMES_2},
"9.1.4": {"id": 19, "names": _NAMES_9_1 + _HEIGHT_NAMES_4},
"9.1.6": {"id": 20, "names": _NAMES_9_1 + _HEIGHT_NAMES_6},
}
@dataclasses.dataclass
class Config:
mode: str
gst_exe_path: pathlib.Path
wav_channels_config: str | None
wav_no_numbers_in_filename: bool
wav_single_thread_decode: bool
cavernize_exe_path: pathlib.Path | None
ffmpeg_exe_path: pathlib.Path
atmos_fps: float
atmos_tool_name: str
atmos_tool_version: str
class AtmosDecode:
def __init__(self, config: Config) -> None:
self.config: Config = config
self.gst_exe_path: pathlib.Path | None = None
self.custom_env = os.environ.copy()
if self.config.mode in ["wav", "thd_atmos_files", "binaural"]:
resolved_exe_path_str = shutil.which(str(self.config.gst_exe_path))
if resolved_exe_path_str:
self.gst_exe_path = pathlib.Path(resolved_exe_path_str).resolve()
else:
direct_path = self.config.gst_exe_path.resolve()
if direct_path.is_file():
self.gst_exe_path = direct_path
else:
raise RuntimeError(
f"GStreamer executable '{self.config.gst_exe_path}' "
f"not found in PATH or as a direct file path."
)
self._setup_gstreamer_env()
def _setup_gstreamer_env(self) -> None:
app_root_dir = None
gst_launch_resolved_path = self.gst_exe_path
assert gst_launch_resolved_path
candidate_app_root = None
for parent_dir in gst_launch_resolved_path.parents:
if parent_dir.name.endswith(".app"):
candidate_app_root = parent_dir
break
if candidate_app_root:
print(f"Info: Detected macOS .app bundle: {app_root_dir}")
core_plugins_path = (
candidate_app_root
/ "Contents/Frameworks/GStreamer.framework/Versions/Current/Resources/lib/gstreamer-1.0/"
)
dolby_plugins_path = candidate_app_root / "Contents/PlugIns/gst-plugins/"
gst_plugin_path_parts = []
if core_plugins_path.is_dir():
gst_plugin_path_parts.append(str(core_plugins_path.resolve()))
else:
print(
f"Warning: Core GStreamer plugins directory not found in .app bundle: {core_plugins_path}",
file=sys.stderr,
)
if dolby_plugins_path.is_dir():
gst_plugin_path_parts.append(str(dolby_plugins_path.resolve()))
else:
print(
f"Warning: Dolby GStreamer plugins directory not found in .app bundle: {dolby_plugins_path}",
file=sys.stderr,
)
if gst_plugin_path_parts:
combined_plugin_path = ":".join(gst_plugin_path_parts)
existing_env_gst_plugin_path = self.custom_env.get("GST_PLUGIN_PATH")
self.custom_env["GST_PLUGIN_PATH"] = (
f"{combined_plugin_path}:{existing_env_gst_plugin_path}"
if existing_env_gst_plugin_path
else combined_plugin_path
)
print(
f"Info: Automatically setting GST_PLUGIN_PATH for .app bundle: {self.custom_env['GST_PLUGIN_PATH']}"
)
elif not (core_plugins_path.is_dir() and dolby_plugins_path.is_dir()):
print(
"Warning: Detected macOS .app bundle, but required GStreamer plugin subdirectories were not found.",
file=sys.stderr,
)
bin_dir = gst_launch_resolved_path.parent
if bin_dir.name == "bin":
resources_dir = bin_dir.parent
if resources_dir.name == "Resources":
scanner_executable_path = (
resources_dir / "libexec" / "gst-plugin-scanner"
)
if scanner_executable_path.is_file() and os.access(
scanner_executable_path, os.X_OK
):
self.custom_env["GST_PLUGIN_SCANNER"] = str(
scanner_executable_path.resolve()
)
print(
f"Info: Automatically setting GST_PLUGIN_SCANNER for .app bundle: {self.custom_env['GST_PLUGIN_SCANNER']}"
)
else:
print(
f"Warning: gst-plugin-scanner not found or not executable at {scanner_executable_path}.",
file=sys.stderr,
)
else:
print(
f"Warning: Could not derive 'Resources' directory from {gst_launch_resolved_path}.",
file=sys.stderr,
)
else:
print(
f"Warning: gst-launch executable not in expected 'bin' subdirectory: {gst_launch_resolved_path}.",
file=sys.stderr,
)
else:
self._setup_fallback_plugin_logic()
def _setup_fallback_plugin_logic(self) -> None:
print(
"Info: Using default GStreamer plugin discovery. Ensure GStreamer and plugins are correctly installed/configured "
"or set GST_PLUGIN_PATH/GST_PLUGIN_SCANNER manually if issues arise."
)
def _prepare_file_path_for_cli(self, source_path: pathlib.Path) -> str:
return str(source_path.resolve())
def _build_gstreamer_eac3_decode_cmd(
self,
input_file: pathlib.Path,
out_file: pathlib.Path,
channel_id: int,
out_channel_config_id: int,
) -> list[str]:
if not self.gst_exe_path:
raise RuntimeError("GStreamer executable path not set.")
return [
str(self.gst_exe_path),
"filesrc",
f"location={self._prepare_file_path_for_cli(input_file)}",
"!",
"dlbac3parse",
"!",
"dlbaudiodecbin",
f"out-ch-config={out_channel_config_id}",
"!",
"deinterleave",
"name=d",
f"d.src_{channel_id}",
"!",
"wavenc",
"!",
"filesink",
f"location={self._prepare_file_path_for_cli(out_file)}",
]
def _build_gstreamer_truehd_decode_cmd(
self,
input_file: pathlib.Path,
out_file: pathlib.Path,
channel_id: int,
out_channel_config_id: int,
) -> list[str]:
if not self.gst_exe_path:
raise RuntimeError("GStreamer executable path not set.")
return [
str(self.gst_exe_path),
"filesrc",
f"location={self._prepare_file_path_for_cli(input_file)}",
"!",
"dlbtruehdparse",
"align-major-sync=false",
"!",
"dlbaudiodecbin",
"truehddec-presentation=16",
f"out-ch-config={out_channel_config_id}",
"!",
"deinterleave",
"name=d",
f"d.src_{channel_id}",
"!",
"wavenc",
"!",
"filesink",
f"location={self._prepare_file_path_for_cli(out_file)}",
]
def _build_gstreamer_truehd_to_pcm_meta_cmd(
self,
input_file: pathlib.Path,
out_pcm_file: pathlib.Path,
out_metadata_file: pathlib.Path,
) -> list[str]:
if not self.gst_exe_path:
raise RuntimeError("GStreamer executable path not set.")
return [
str(self.gst_exe_path),
"filesrc",
f"location={self._prepare_file_path_for_cli(input_file)}",
"!",
"dlbtruehdparse",
"align-major-sync=false",
"!",
"dlbtruehddec",
"out-ch-config=21",
"presentation=16",
"!",
"tee",
"name=t",
"!",
"queue",
"!",
"oamdcapsfeature",
"remove=true",
"!",
"audioconvert",
"!",
"audio/x-raw,format=S24LE,layout=interleaved",
"!",
"filesink",
f"location={self._prepare_file_path_for_cli(out_pcm_file)}",
"t.",
"!",
"queue",
"!",
"oamdserialize",
"!",
"filesink",
f"location={self._prepare_file_path_for_cli(out_metadata_file)}",
]
def _build_cavernize_eac3_to_adm_cmd(
self, input_file: pathlib.Path, out_adm_bwf_file: pathlib.Path
) -> list[str]:
if not self.config.cavernize_exe_path:
raise RuntimeError(
"Path to CavernizeGUI.exe not specified (--cavernize_exe_path)."
)
cavernize_resolved_path = self.config.cavernize_exe_path.resolve()
if not cavernize_resolved_path.is_file():
raise RuntimeError(
f"CavernizeGUI.exe not found at: {cavernize_resolved_path}"
)
wine_exe_path = shutil.which("wine")
if not wine_exe_path:
raise RuntimeError(
"Wine executable not found in PATH. Wine is required for CavernizeGUI on macOS."
)
return [
wine_exe_path,
str(cavernize_resolved_path),
"-i",
self._prepare_file_path_for_cli(input_file),
"-f",
"ADM_BWF_Atmos",
"-o",
self._prepare_file_path_for_cli(out_adm_bwf_file),
]
def _build_gstreamer_truehd_binaural_cmd(
self, input_file: pathlib.Path, out_file: pathlib.Path
) -> list[str]:
if not self.gst_exe_path:
raise RuntimeError("GStreamer executable path not set.")
return [
str(self.gst_exe_path),
"filesrc",
f"location={self._prepare_file_path_for_cli(input_file)}",
"!",
"dlbtruehdparse",
"align-major-sync=false",
"!",
"dlbtruehddec",
"out-ch-config=21",
"presentation=16",
"!",
"oamdcapsfeature",
"float=true",
"!",
"dlbdapvr",
"!",
"wavenc",
"!",
"filesink",
f"location={self._prepare_file_path_for_cli(out_file)}",
]
def _build_gstreamer_eac3_binaural_cmd(
self, input_file: pathlib.Path, out_file: pathlib.Path
) -> list[str]:
if not self.gst_exe_path:
raise RuntimeError("GStreamer executable path not set.")
return [
str(self.gst_exe_path),
"filesrc",
f"location={self._prepare_file_path_for_cli(input_file)}",
"!",
"dlbac3parse",
"!",
"dlbac3dec",
"out-ch-config=21",
"!",
"oamdcapsfeature",
"float=true",
"!",
"dlbdapvr",
"!",
"wavenc",
"!",
"filesink",
f"location={self._prepare_file_path_for_cli(out_file)}",
]
def _generate_atmos_file_content(
self, metadata_filename: str, caf_filename: str
) -> str:
object_ids_str = "\n".join([f" - ID: {i}" for i in range(10, 25)])
return f"""
version: 0.5.1
presentations:
- type: home
simplified: false
metadata: {metadata_filename}
audio: {caf_filename}
offset: 0
fps: {self.config.atmos_fps}
scBedConfiguration: [3]
creationTool: {self.config.atmos_tool_name}
creationToolVersion: {self.config.atmos_tool_version}
downmixType_5to2: LoRo_Stereo
51-to-20_LsRs90degPhaseShift: false
warpMode: warping
bedInstances:
- channels:
- channel: LFE
ID: 3
objects:
{object_ids_str}
"""
def decode(
self, input_file: pathlib.Path, output_base_path: pathlib.Path | None = None
):
if not input_file.is_file():
raise RuntimeError(
f"Input file {input_file.resolve()} is not a regular file."
)
is_eac3, is_truehd = False, False
try:
with input_file.open("rb") as f:
leading_bytes = f.read(10)
if leading_bytes.startswith(b"\x0b\x77"):
is_eac3 = True
f.seek(0)
content_sample = f.read(2048)
if b"\xf8\x72\x6f\xba" in content_sample:
is_truehd = True
except Exception as e:
print(
f"Warning: Error reading input file for type detection: {e}. Proceeding.",
file=sys.stderr,
)
if not is_eac3 and not is_truehd:
print(
"Warning: Could not definitively identify input file type. Proceeding with selected mode.",
file=sys.stderr,
)
if self.config.mode == "wav":
if not self.config.wav_channels_config:
raise RuntimeError(
"Channel configuration ('--wav_channels_config') is required for 'wav' mode."
)
if not (is_eac3 or is_truehd):
raise RuntimeError(
"'wav' mode requires identifiable E-AC3 or TrueHD input."
)
gst_cmd_builder = (
self._build_gstreamer_eac3_decode_cmd
if is_eac3
else self._build_gstreamer_truehd_decode_cmd
)
print(
f"Info: Detected {'E-AC3' if is_eac3 else 'TrueHD'} input for WAV decoding."
)
layout_info = CHANNELS[self.config.wav_channels_config]
gst_out_ch_id = layout_info["id"]
out_ch_names = layout_info["names"]
procs = []
for i, ch_name in enumerate(out_ch_names):
suffix = (
f".{ch_name}.wav"
if self.config.wav_no_numbers_in_filename
else f".{str(i + 1).zfill(2)}_{ch_name}.wav"
)
out_fpath = (
output_base_path.with_suffix(suffix)
if output_base_path
else input_file.with_suffix(suffix)
)
gst_cmd = gst_cmd_builder(input_file, out_fpath, i, gst_out_ch_id)
if self.config.wav_single_thread_decode:
print(f'Decoding "{out_fpath.name}" sequentially...')
subprocess.run(gst_cmd, check=True, env=self.custom_env)
else:
print(f"Starting parallel decode for: {out_fpath.name}")
procs.append(subprocess.Popen(gst_cmd, env=self.custom_env))
for i, p in enumerate(procs):
p.wait()
if p.returncode != 0:
print(
f"Warning: Decode process for channel {i+1} ({p.args}) failed (code {p.returncode}).",
file=sys.stderr,
)
print("Info: WAV decoding tasks submitted/completed.")
elif self.config.mode == "thd_atmos_files":
if not is_truehd:
print(
"Warning: Input not definitively TrueHD. GStreamer will attempt PCM+metadata extraction.",
file=sys.stderr,
)
out_fname_base = input_file.stem
out_dir = pathlib.Path.cwd()
if output_base_path:
out_fname_base = output_base_path.stem
out_dir = output_base_path.parent
if not out_dir.exists():
out_dir.mkdir(parents=True, exist_ok=True)
out_pcm_file = out_dir / f"{out_fname_base}.pcm"
out_metadata_file = out_dir / f"{out_fname_base}.metadata"
out_caf_file = out_dir / f"{out_fname_base}.caf"
out_atmos_file = out_dir / f"{out_fname_base}.atmos"
print(f'Info: Decoding "{input_file.name}" to PCM and Atmos Metadata...')
gst_cmd = self._build_gstreamer_truehd_to_pcm_meta_cmd(
input_file, out_pcm_file, out_metadata_file
)
subprocess.run(gst_cmd, check=True, env=self.custom_env)
print(f" PCM output: {out_pcm_file.name}")
print(f" Metadata output: {out_metadata_file.name}")
ffmpeg_exe = shutil.which(str(self.config.ffmpeg_exe_path))
if not ffmpeg_exe:
raise RuntimeError(
f"ffmpeg executable not found at '{self.config.ffmpeg_exe_path}' or in PATH."
)
print(
f"Info: Converting PCM '{out_pcm_file.name}' to CAF '{out_caf_file.name}' using ffmpeg..."
)
ffmpeg_cmd = [
ffmpeg_exe,
"-y",
"-f",
"s24le",
"-ar",
"48k",
"-ac",
"16",
"-i",
str(out_pcm_file),
"-c:a",
"copy",
str(out_caf_file),
]
subprocess.run(ffmpeg_cmd, check=True)
print(f" CAF output: {out_caf_file.name}")
print(f"Info: Generating .atmos file '{out_atmos_file.name}'...")
atmos_content = self._generate_atmos_file_content(
out_metadata_file.name, out_caf_file.name
)
with open(out_atmos_file, "w", encoding="utf-8") as f:
f.write(atmos_content)
print(f" .atmos output: {out_atmos_file.name}")
print(
"Info: TrueHD to Atmos files generation complete. Outputs: .atmos, .metadata, .caf (and .pcm)."
)
elif self.config.mode == "eac3_adm_bwf":
if not is_eac3:
print(
"Warning: Input not definitively E-AC3. CavernizeGUI will attempt processing.",
file=sys.stderr,
)
out_adm_file = (
output_base_path.with_suffix(".wav")
if output_base_path
else input_file.with_name(f"{input_file.stem}_ADM_BWF.wav")
)
cav_cmd = self._build_cavernize_eac3_to_adm_cmd(input_file, out_adm_file)
print(
f'Info: Decoding "{input_file.name}" to ADM BWF using CavernizeGUI...'
)
print(f" Output ADM BWF: {out_adm_file.name}")
print(f"Executing: {' '.join(map(str,cav_cmd))}")
subprocess.run(cav_cmd, check=True, env=self.custom_env)
print("Info: E-AC3 to ADM BWF decoding completed.")
elif self.config.mode == "binaural":
if not (is_eac3 or is_truehd):
raise RuntimeError(
"'binaural' mode requires identifiable E-AC3 or TrueHD input."
)
print(
f"Info: Detected {'E-AC3' if is_eac3 else 'TrueHD'} input for binaural rendering."
)
out_fpath = (
output_base_path.with_suffix(".wav")
if output_base_path
else input_file.with_suffix(".binaural.wav")
)
gst_cmd_builder = (
self._build_gstreamer_eac3_binaural_cmd
if is_eac3
else self._build_gstreamer_truehd_binaural_cmd
)
gst_cmd = gst_cmd_builder(input_file, out_fpath)
print(f'Decoding "{out_fpath.name}"...')
subprocess.run(gst_cmd, check=True, env=self.custom_env)
print("Info: Binaural rendering completed.")
else:
raise RuntimeError(f"Unknown processing mode: {self.config.mode}")
def main():
available_modes = ["wav", "eac3_adm_bwf", "thd_atmos_files", "binaural"]
parser = argparse.ArgumentParser(
description=f"{SCRIPT_NAME} v{SCRIPT_VERSION} - Decodes Dolby Atmos audio files on macOS.",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"-i",
"--input",
help="Path to source audio file (E-AC3 or TrueHD).",
type=pathlib.Path,
required=True,
)
parser.add_argument(
"-o",
"--output",
type=pathlib.Path,
help="Base path/name for output file(s).\n"
'- "wav" mode: Suffixes like ".01_L.wav" are added. Default: beside input.\n'
'- "thd_atmos_files" mode: Base for ".atmos", ".metadata", ".caf", ".pcm". Default: beside input.\n'
'- "eac3_adm_bwf" mode: Full path for ".wav" ADM BWF. Default: "input_ADM_BWF.wav" beside input.\n'
'- "binaural" mode: Full path for binaural ".wav" output. Default: "input.binaural.wav"',
)
parser.add_argument(
"--mode",
default="wav",
choices=available_modes,
type=str,
help="Processing mode:\n"
" wav: (Default) Decode to individual channel WAV files (GStreamer).\n"
" thd_atmos_files: Decode TrueHD to .atmos, .metadata, .caf, and .pcm files (GStreamer + ffmpeg).\n"
" eac3_adm_bwf: Decode E-AC3 to ADM BWF (.wav) (CavernizeGUI + Wine).\n"
" binaural: Binaural rendering",
)
gst_group = parser.add_argument_group(
'GStreamer Options (for "wav", "thd_atmos_files", "binaural")'
)
gst_group.add_argument(
"--gst_exe_path",
help="Path to 'gst-launch-1.0'. Default: '/Applications/Dolby/Dolby Reference Player.app/Contents/Frameworks/GStreamer.framework/Versions/1_16/Resources/bin/gst-launch-1.0'.",
type=pathlib.Path,
default=pathlib.Path(
"/Applications/Dolby/Dolby Reference Player.app/Contents/Frameworks/GStreamer.framework/Versions/1_16/Resources/bin/gst-launch-1.0"
),
)
wav_group = parser.add_argument_group('Options for "wav" mode')
wav_group.add_argument(
"-c",
"--wav_channels_config",
help='Output channel config (e.g., 7.1.4). Required for "wav" mode.',
type=str,
choices=CHANNELS.keys(),
)
wav_group.add_argument(
"--wav_no_numbers_in_filename",
help='Omit numeric prefixes in WAV filenames (e.g., "L.wav").',
action="store_true",
)
wav_group.add_argument(
"--wav_single_thread_decode",
help="Decode WAV channels sequentially (slower, less CPU).",
action="store_true",
)
cav_group = parser.add_argument_group('Options for "eac3_adm_bwf" mode')
cav_group.add_argument(
"--cavernize_exe_path",
help='Path to CavernizeGUI.exe. Required for "eac3_adm_bwf".',
type=pathlib.Path,
)
atmos_files_group = parser.add_argument_group('Options for "thd_atmos_files" mode')
atmos_files_group.add_argument(
"--ffmpeg_path",
type=pathlib.Path,
default=pathlib.Path("ffmpeg"),
help="Path to 'ffmpeg' executable. Default: 'ffmpeg' (from PATH).",
)
atmos_files_group.add_argument(
"--atmos_fps",
type=float,
default=24.0,
help="FPS for .atmos file. Default: 24.0.",
)
atmos_files_group.add_argument(
"--atmos_tool_name",
type=str,
default="YUYGFGG",
help="Creation tool name for .atmos file. Default: YUYGFGG.",
)
atmos_files_group.add_argument(
"--atmos_tool_version",
type=str,
default=SCRIPT_VERSION,
help=f"Creation tool version for .atmos file. Default: {SCRIPT_VERSION}.",
)
args = parser.parse_args()
if args.mode == "wav" and not args.wav_channels_config:
parser.error("--wav_channels_config is required for 'wav' mode.")
if args.mode == "eac3_adm_bwf" and not args.cavernize_exe_path:
parser.error("--cavernize_exe_path is required for 'eac3_adm_bwf' mode.")
script_config = Config(
mode=args.mode,
gst_exe_path=args.gst_exe_path,
wav_channels_config=args.wav_channels_config,
wav_no_numbers_in_filename=args.wav_no_numbers_in_filename,
wav_single_thread_decode=args.wav_single_thread_decode,
cavernize_exe_path=args.cavernize_exe_path,
ffmpeg_exe_path=args.ffmpeg_path,
atmos_fps=args.atmos_fps,
atmos_tool_name=args.atmos_tool_name,
atmos_tool_version=args.atmos_tool_version,
)
try:
decoder = AtmosDecode(script_config)
decoder.decode(args.input, args.output)
print(f"Info: Processing via mode '{args.mode}' completed successfully.")
except RuntimeError as e:
raise RuntimeError(e)
except subprocess.CalledProcessError as e:
cmd_str = " ".join(map(str, e.cmd)) if isinstance(e.cmd, list) else str(e.cmd)
stdout = (
f"Stdout:\n{e.stdout.decode(errors='replace') if isinstance(e.stdout, bytes) else str(e.stdout)}"
if e.stdout
else "Stdout: (Empty)"
)
stderr = (
f"Stderr:\n{e.stderr.decode(errors='replace') if isinstance(e.stderr, bytes) else str(e.stderr)}"
if e.stderr
else "Stderr: (Empty)"
)
raise RuntimeError(
f"External command execution failed: {cmd_str}\n"
f"Return code: {e.returncode}\n"
f"{stdout}\n"
f"{stderr}"
)
except Exception as e:
raise RuntimeError(f"An unexpected error occurred: {e}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment