Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ jobs:
auto-update-conda: true
conda-solver: libmamba

- name: Ensure conda 'defaults' channel not present
run: |
# Only remove the 'defaults' channel if it exists to avoid CondaKeyError
if conda config --show channels | grep -q 'defaults'; then
conda config --remove channels defaults
echo "Removed 'defaults' channel"
else
echo "'defaults' channel not present, skipping removal"
fi

- name: Install dependencies
run: |
poetry check
Expand All @@ -56,7 +66,7 @@ jobs:
if: ${{ github.event_name == 'pull_request' }}
run: |
pre-commit install
pre-commit run --all-file --verbose
pre-commit run --all-files --verbose

- name: Build the book
run: |
Expand All @@ -69,4 +79,4 @@ jobs:
if: ${{ github.event_name == 'push' }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build
publish_dir: ./build
29 changes: 29 additions & 0 deletions src/artbox/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# ...existing code...
from artbox.validators import validate_io_paths
import sys
# ...existing code...

# INSERT: extract common arg names and validate extensions
input_path = getattr(args, "input_path", None) or getattr(args, "input", None) or None
output_path = getattr(args, "output_path", None) or getattr(args, "output", None) or None

operation = None
for attr in ("operation", "op", "command", "cmd", "subcommand", "mode"):
val = getattr(args, attr, None)
if isinstance(val, str) and val:
operation = val
break

if isinstance(operation, str):
ol = operation.lower()
if ol in ("tts", "text-to-speech", "text->speech"):
operation = "text-to-speech"
elif ol in ("stt", "speech-to-text", "speech->text"):
operation = "speech-to-text"

try:
validate_io_paths(input_path, output_path, operation)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
raise SystemExit(2)
# ...existing code...
141 changes: 141 additions & 0 deletions src/artbox/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from pathlib import Path
from typing import Optional

AUDIO_EXTS = {".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac"}
TEXT_EXTS = {".txt", ".md", ".srt", ".vtt", ".json"}


def _ext_of(path: Optional[str]) -> str:
return Path(path).suffix.lower() if path else ""


def validate_io_paths(input_path: Optional[str], output_path: Optional[str], operation: Optional[str] = None) -> None:
"""
Validate input/output extensions for common operations.

operation can be:
- 'text-to-speech' / 'text->speech' / 'tts'
- 'speech-to-text' / 'speech->text' / 'stt'
- None (best-effort inference)

Raises ValueError on invalid combinations.
"""
op = (operation or "").lower()
in_ext = _ext_of(input_path)
out_ext = _ext_of(output_path)

tts_ops = {"text-to-speech", "text->speech", "tts"}
stt_ops = {"speech-to-text", "speech->text", "stt"}

if op in tts_ops:
if not output_path or out_ext == "":
raise ValueError("text->speech requires an audio output file (e.g. --output-path out.mp3).")
if out_ext not in AUDIO_EXTS:
raise ValueError(f"Invalid output audio format '{out_ext}'. Supported: {', '.join(sorted(AUDIO_EXTS))}")

elif op in stt_ops:
if not input_path or in_ext == "" or in_ext not in AUDIO_EXTS:
raise ValueError(f"speech->text requires an audio input file (supported: {', '.join(sorted(AUDIO_EXTS))}).")
if not output_path or out_ext == "" or out_ext not in TEXT_EXTS:
raise ValueError(f"speech->text requires a text output file (supported: {', '.join(sorted(TEXT_EXTS))}).")

else:
# Infer operation by extensions and validate.
inferred_op = None
if in_ext:
if in_ext in AUDIO_EXTS:
inferred_op = "speech-to-text"
elif in_ext in TEXT_EXTS:
inferred_op = "text-to-speech"
if not inferred_op and out_ext:
if out_ext in AUDIO_EXTS:
inferred_op = "text-to-speech"
elif out_ext in TEXT_EXTS:
inferred_op = "speech-to-text"

if inferred_op == "text-to-speech":
if not output_path or out_ext == "":
raise ValueError("text->speech requires an audio output file (e.g. --output-path out.mp3).")
if out_ext not in AUDIO_EXTS:
raise ValueError(f"Invalid output audio format '{out_ext}'. Supported: {', '.join(sorted(AUDIO_EXTS))}")
elif inferred_op == "speech-to-text":
if not input_path or in_ext == "" or in_ext not in AUDIO_EXTS:
raise ValueError(f"speech->text requires an audio input file (supported: {', '.join(sorted(AUDIO_EXTS))}).")
if not output_path or out_ext == "" or out_ext not in TEXT_EXTS:
raise ValueError(f"speech->text requires a text output file (supported: {', '.join(sorted(TEXT_EXTS))}).")
else:
if input_path and in_ext and in_ext not in AUDIO_EXTS.union(TEXT_EXTS):
raise ValueError(f"Unsupported input extension '{in_ext}'. Supported: audio {', '.join(sorted(AUDIO_EXTS))} or text {', '.join(sorted(TEXT_EXTS))}.")
if output_path and out_ext and out_ext not in AUDIO_EXTS.union(TEXT_EXTS):
raise ValueError(f"Unsupported output extension '{out_ext}'. Supported: audio {', '.join(sorted(AUDIO_EXTS))} or text {', '.join(sorted(TEXT_EXTS))}.")

from pathlib import Path
from typing import Optional

AUDIO_EXTS = {".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac"}
TEXT_EXTS = {".txt", ".md", ".srt", ".vtt", ".json"}


def _ext_of(path: Optional[str]) -> str:
return Path(path).suffix.lower() if path else ""


def validate_io_paths(input_path: Optional[str], output_path: Optional[str], operation: Optional[str] = None) -> None:
"""
Validate input/output extensions for common operations.

operation can be:
- 'text-to-speech' / 'text->speech' / 'tts'
- 'speech-to-text' / 'speech->text' / 'stt'
- None (best-effort inference)

Raises ValueError on invalid combinations.
"""
op = (operation or "").lower()
in_ext = _ext_of(input_path)
out_ext = _ext_of(output_path)

tts_ops = {"text-to-speech", "text->speech", "tts"}
stt_ops = {"speech-to-text", "speech->text", "stt"}

if op in tts_ops:
if not output_path or out_ext == "":
raise ValueError("text->speech requires an audio output file (e.g. --output-path out.mp3).")
if out_ext not in AUDIO_EXTS:
raise ValueError(f"Invalid output audio format '{out_ext}'. Supported: {', '.join(sorted(AUDIO_EXTS))}")

elif op in stt_ops:
if not input_path or in_ext == "" or in_ext not in AUDIO_EXTS:
raise ValueError(f"speech->text requires an audio input file (supported: {', '.join(sorted(AUDIO_EXTS))}).")
if not output_path or out_ext == "" or out_ext not in TEXT_EXTS:
raise ValueError(f"speech->text requires a text output file (supported: {', '.join(sorted(TEXT_EXTS))}).")

else:
# Infer operation by extensions and validate.
inferred_op = None
if in_ext:
if in_ext in AUDIO_EXTS:
inferred_op = "speech-to-text"
elif in_ext in TEXT_EXTS:
inferred_op = "text-to-speech"
if not inferred_op and out_ext:
if out_ext in AUDIO_EXTS:
inferred_op = "text-to-speech"
elif out_ext in TEXT_EXTS:
inferred_op = "speech-to-text"

if inferred_op == "text-to-speech":
if not output_path or out_ext == "":
raise ValueError("text->speech requires an audio output file (e.g. --output-path out.mp3).")
if out_ext not in AUDIO_EXTS:
raise ValueError(f"Invalid output audio format '{out_ext}'. Supported: {', '.join(sorted(AUDIO_EXTS))}")
elif inferred_op == "speech-to-text":
if not input_path or in_ext == "" or in_ext not in AUDIO_EXTS:
raise ValueError(f"speech->text requires an audio input file (supported: {', '.join(sorted(AUDIO_EXTS))}).")
if not output_path or out_ext == "" or out_ext not in TEXT_EXTS:
raise ValueError(f"speech->text requires a text output file (supported: {', '.join(sorted(TEXT_EXTS))}).")
else:
if input_path and in_ext and in_ext not in AUDIO_EXTS.union(TEXT_EXTS):
raise ValueError(f"Unsupported input extension '{in_ext}'. Supported: audio {', '.join(sorted(AUDIO_EXTS))} or text {', '.join(sorted(TEXT_EXTS))}.")
if output_path and out_ext and out_ext not in AUDIO_EXTS.union(TEXT_EXTS):
raise ValueError(f"Unsupported output extension '{out_ext}'. Supported: audio {', '.join(sorted(AUDIO_EXTS))} or text {', '.join(sorted(TEXT_EXTS))}.")
Loading