From 2caccc183cd2e157e9b6de82519947b262b819ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 8 Mar 2026 16:42:44 +0000 Subject: [PATCH] generalize convert-to-mp3 into convert-audio with selectable target format - rename task to convert-audio, support mp3/flac/wav/ogg/aac/opus targets - add options input type with selectable pill buttons in UI - extend seed script parser to handle YAML list properties (options) Co-Authored-By: Claude Opus 4.6 --- .../{convert-to-mp3 => convert-audio}/TASK.md | 20 ++++++++--- .../{convert-to-mp3 => convert-audio}/run.sh | 28 +++++++++++---- src/databases/officer_db/seed-tasks.ts | 36 ++++++++++++++++--- .../components/TaskRunnerModal.tsx | 21 +++++++++++ 4 files changed, 91 insertions(+), 14 deletions(-) rename seed/tasks/{convert-to-mp3 => convert-audio}/TASK.md (61%) rename seed/tasks/{convert-to-mp3 => convert-audio}/run.sh (63%) diff --git a/seed/tasks/convert-to-mp3/TASK.md b/seed/tasks/convert-audio/TASK.md similarity index 61% rename from seed/tasks/convert-to-mp3/TASK.md rename to seed/tasks/convert-audio/TASK.md index 87443309..55e356e2 100644 --- a/seed/tasks/convert-to-mp3/TASK.md +++ b/seed/tasks/convert-audio/TASK.md @@ -1,12 +1,13 @@ --- -name: Convert To MP3 -description: Convert audio files to MP3 320kbps, preserving metadata. +name: Convert Audio +description: Convert audio files between formats, preserving metadata. version: 1 mode: script language: bash triggers: - type: file extensions: + - mp3 - flac - wav - ogg @@ -26,6 +27,17 @@ inputs: file_path: type: string description: Path to an audio file or directory to convert. + target_format: + type: string + description: Target format + default: mp3 + options: + - mp3 + - flac + - wav + - ogg + - aac + - opus delete_source: type: boolean description: Delete source files after successful conversion. @@ -33,7 +45,7 @@ inputs: args: [file_path] --- -# Convert To MP3 +# Convert Audio -Convert audio files to MP3 320kbps using ffmpeg, preserving metadata. +Convert audio files between formats using ffmpeg, preserving metadata. Supports single file conversion and batch directory conversion. diff --git a/seed/tasks/convert-to-mp3/run.sh b/seed/tasks/convert-audio/run.sh similarity index 63% rename from seed/tasks/convert-to-mp3/run.sh rename to seed/tasks/convert-audio/run.sh index 86080f21..e2dce3d0 100755 --- a/seed/tasks/convert-to-mp3/run.sh +++ b/seed/tasks/convert-audio/run.sh @@ -1,11 +1,26 @@ #!/usr/bin/env bash set -euo pipefail -# ── Task: Convert To MP3 ── +# ── Task: Convert Audio ── -EXTENSIONS="flac|wav|ogg|wma|aac|m4a|opus|aiff|aif|ape|wv|alac|dsf|dff" +EXTENSIONS="mp3|flac|wav|ogg|wma|aac|m4a|opus|aiff|aif|ape|wv|alac|dsf|dff" +TARGET_FORMAT="${INPUT_TARGET_FORMAT:-mp3}" DELETE_SOURCE="${INPUT_DELETE_SOURCE:-false}" +ffmpeg_args_for_format() { + case "$1" in + mp3) echo "-codec:a libmp3lame -b:a 320k -id3v2_version 3" ;; + flac) echo "-codec:a flac" ;; + wav) echo "-codec:a pcm_s16le" ;; + ogg) echo "-codec:a libvorbis -q:a 6" ;; + aac) echo "-codec:a aac -b:a 256k" ;; + opus) echo "-codec:a libopus -b:a 192k" ;; + *) echo "Unsupported format: $1" >&2; return 1 ;; + esac +} + +FFMPEG_ARGS=$(ffmpeg_args_for_format "$TARGET_FORMAT") || exit 1 + process_file() { local input="$1" local ext="${input##*.}" @@ -13,10 +28,10 @@ process_file() { dir="$(dirname "$input")" local base base="$(basename "$input" ".$ext")" - local output="$dir/$base.mp3" + local output="$dir/$base.$TARGET_FORMAT" - if [[ "${ext,,}" == "mp3" ]]; then - echo "Skipping (already MP3): $input" + if [[ "${ext,,}" == "$TARGET_FORMAT" ]]; then + echo "Skipping (already $TARGET_FORMAT): $input" return 0 fi @@ -26,7 +41,8 @@ process_file() { fi echo "Converting: $input → $output" - if ffmpeg -nostdin -i "$input" -codec:a libmp3lame -b:a 320k -map_metadata 0 -id3v2_version 3 -y "$output" 2>/dev/null; then + # shellcheck disable=SC2086 + if ffmpeg -nostdin -i "$input" $FFMPEG_ARGS -map_metadata 0 -y "$output" 2>/dev/null; then echo " Done" if [[ "$DELETE_SOURCE" == "true" ]]; then rm "$input" diff --git a/src/databases/officer_db/seed-tasks.ts b/src/databases/officer_db/seed-tasks.ts index 07e5b075..cf13a08c 100644 --- a/src/databases/officer_db/seed-tasks.ts +++ b/src/databases/officer_db/seed-tasks.ts @@ -48,24 +48,52 @@ function parseFrontmatter(content: string) { } meta.trigger = triggers.length > 0 ? triggers : undefined; - // Parse inputs (simplified — store as-is for now) + // Parse inputs const inputsMatch = yaml.match(/^inputs:\s*\n((?:[ \t]+.+\n?)*)/m); if (inputsMatch) { const inputBlock = inputsMatch[1]!; - const inputEntries: Record> = {}; + const inputEntries: Record> = {}; let currentInput: string | null = null; + let currentListKey: string | null = null; + let currentList: string[] = []; + + const flushList = () => { + if (currentInput && currentListKey && currentList.length > 0) { + inputEntries[currentInput]![currentListKey] = currentList; + } + currentListKey = null; + currentList = []; + }; + for (const line of inputBlock.split('\n')) { const topMatch = line.match(/^\s{2}(\w[\w_-]*):\s*$/); if (topMatch) { + flushList(); currentInput = topMatch[1]!; inputEntries[currentInput] = {}; continue; } - const propMatch = line.match(/^\s{4}(\w[\w_-]*):\s*(.+)$/); + // List item (6 spaces + dash) + const listItemMatch = line.match(/^\s{6}-\s*(.+)$/); + if (listItemMatch && currentInput && currentListKey) { + currentList.push(listItemMatch[1]!.trim()); + continue; + } + // Property with value or start of list + const propMatch = line.match(/^\s{4}(\w[\w_-]*):\s*(.*)$/); if (propMatch && currentInput) { - inputEntries[currentInput]![propMatch[1]!] = propMatch[2]!.trim(); + flushList(); + const value = propMatch[2]!.trim(); + if (value === '') { + // Start of a list (e.g. "options:") + currentListKey = propMatch[1]!; + currentList = []; + } else { + inputEntries[currentInput]![propMatch[1]!] = value; + } } } + flushList(); if (Object.keys(inputEntries).length > 0) { meta.inputs = inputEntries; } diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx index e9acc720..d091a584 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx @@ -198,6 +198,7 @@ type TaskInputDef = { type: string; description?: string; default?: string; + options?: string[]; }; type TaskInputFormProps = { @@ -239,6 +240,26 @@ const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys }: TaskInpu ); } + if (def.options && def.options.length > 0) { + const selected = values[key] ?? def.default ?? def.options[0]!; + return ( +
+ {def.description ?? key} +
+ {def.options.map((opt) => ( + + ))} +
+
+ ); + } + // Default: text input for string/number return (