diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 8bb26a7c..39fc1d76 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -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 @@ -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: | @@ -69,4 +79,4 @@ jobs: if: ${{ github.event_name == 'push' }} with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./build + publish_dir: ./build \ No newline at end of file diff --git a/src/artbox/cli.py b/src/artbox/cli.py new file mode 100644 index 00000000..b49529ae --- /dev/null +++ b/src/artbox/cli.py @@ -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... \ No newline at end of file diff --git a/src/artbox/validators.py b/src/artbox/validators.py new file mode 100644 index 00000000..07c372cd --- /dev/null +++ b/src/artbox/validators.py @@ -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))}.") \ No newline at end of file