audio-classifier-tool 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. audio_classifier_tool-1.2.0/.github/workflows/ci.yml +37 -0
  2. audio_classifier_tool-1.2.0/.github/workflows/publish.yml +24 -0
  3. audio_classifier_tool-1.2.0/.gitignore +24 -0
  4. audio_classifier_tool-1.2.0/.python-version +1 -0
  5. audio_classifier_tool-1.2.0/CLAUDE.md +28 -0
  6. audio_classifier_tool-1.2.0/Dockerfile +36 -0
  7. audio_classifier_tool-1.2.0/LICENSE +21 -0
  8. audio_classifier_tool-1.2.0/PKG-INFO +123 -0
  9. audio_classifier_tool-1.2.0/README.md +92 -0
  10. audio_classifier_tool-1.2.0/docker-compose.yml +9 -0
  11. audio_classifier_tool-1.2.0/examples/configs/ads.toon +6 -0
  12. audio_classifier_tool-1.2.0/examples/configs/politics.toon +5 -0
  13. audio_classifier_tool-1.2.0/examples/process_audio.py +72 -0
  14. audio_classifier_tool-1.2.0/examples/train_classifier.py +25 -0
  15. audio_classifier_tool-1.2.0/pyproject.toml +56 -0
  16. audio_classifier_tool-1.2.0/src/audioclassifier/__init__.py +59 -0
  17. audio_classifier_tool-1.2.0/src/audioclassifier/__main__.py +4 -0
  18. audio_classifier_tool-1.2.0/src/audioclassifier/alerts/__init__.py +1 -0
  19. audio_classifier_tool-1.2.0/src/audioclassifier/alerts/discord_alerts.py +206 -0
  20. audio_classifier_tool-1.2.0/src/audioclassifier/cli.py +147 -0
  21. audio_classifier_tool-1.2.0/src/audioclassifier/cloud/__init__.py +0 -0
  22. audio_classifier_tool-1.2.0/src/audioclassifier/cloud/storage.py +36 -0
  23. audio_classifier_tool-1.2.0/src/audioclassifier/config/__init__.py +0 -0
  24. audio_classifier_tool-1.2.0/src/audioclassifier/config/constants.py +44 -0
  25. audio_classifier_tool-1.2.0/src/audioclassifier/config/detection_config.py +142 -0
  26. audio_classifier_tool-1.2.0/src/audioclassifier/detection/__init__.py +0 -0
  27. audio_classifier_tool-1.2.0/src/audioclassifier/detection/llm_detector.py +240 -0
  28. audio_classifier_tool-1.2.0/src/audioclassifier/detection/text_classifier.py +147 -0
  29. audio_classifier_tool-1.2.0/src/audioclassifier/logger/__init__.py +0 -0
  30. audio_classifier_tool-1.2.0/src/audioclassifier/logger/logger_setup.py +17 -0
  31. audio_classifier_tool-1.2.0/src/audioclassifier/processing/__init__.py +0 -0
  32. audio_classifier_tool-1.2.0/src/audioclassifier/processing/audio_processor.py +371 -0
  33. audio_classifier_tool-1.2.0/src/audioclassifier/processing/download_mp3.py +62 -0
  34. audio_classifier_tool-1.2.0/src/audioclassifier/processing/mp3_handler.py +82 -0
  35. audio_classifier_tool-1.2.0/src/audioclassifier/util.py +11 -0
  36. audio_classifier_tool-1.2.0/tests/README.md +71 -0
  37. audio_classifier_tool-1.2.0/tests/integration/test_package_install.py +53 -0
  38. audio_classifier_tool-1.2.0/tests/integration/test_process_audio.py +32 -0
  39. audio_classifier_tool-1.2.0/tests/unit/cloud/test_storage.py +15 -0
  40. audio_classifier_tool-1.2.0/tests/unit/config/test_detection_config.py +95 -0
  41. audio_classifier_tool-1.2.0/tests/unit/detection/test_llm_detector.py +66 -0
  42. audio_classifier_tool-1.2.0/tests/unit/detection/test_text_classifier.py +87 -0
  43. audio_classifier_tool-1.2.0/tests/unit/processing/test_audio_processor.py +84 -0
  44. audio_classifier_tool-1.2.0/uv.lock +1564 -0
@@ -0,0 +1,37 @@
1
+ name: CI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ test:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+
12
+ - uses: astral-sh/setup-uv@v5
13
+
14
+ - name: Install
15
+ run: uv sync
16
+
17
+ - name: Unit tests
18
+ run: uv run pytest
19
+
20
+ - name: Build and test the installed package
21
+ run: uv run pytest -m integration tests/integration/test_package_install.py
22
+
23
+ docker:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+
28
+ - uses: docker/setup-buildx-action@v3
29
+
30
+ - name: Build Docker image
31
+ uses: docker/build-push-action@v6
32
+ with:
33
+ context: .
34
+ push: false
35
+ tags: audioclassifier:ci
36
+ cache-from: type=gha
37
+ cache-to: type=gha,mode=max
@@ -0,0 +1,24 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ publish:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+
12
+ - uses: astral-sh/setup-uv@v5
13
+
14
+ - name: Install
15
+ run: uv sync
16
+
17
+ - name: Unit tests
18
+ run: uv run pytest
19
+
20
+ - name: Build
21
+ run: uv build
22
+
23
+ - name: Publish
24
+ run: uv publish --token ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,24 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ .venv/
5
+ venv/
6
+ .pytest_cache/
7
+ dist/
8
+
9
+ # App working dirs
10
+ downloads/
11
+ output/
12
+ local_models/
13
+
14
+ # Data artifacts
15
+ *.mp3
16
+ *.wav
17
+ *.log
18
+ *.db
19
+ *.out
20
+ *.json
21
+ *.txt
22
+
23
+ # macOS
24
+ .DS_Store
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,28 @@
1
+ # AudioClassifier
2
+
3
+ Detects and cuts target segments (ads by default) from audio. Installable package (`audioclassifier`) usable as a library (`audioclassifier.process_audio`) or CLI.
4
+
5
+ ## Commands
6
+
7
+ - `uv sync` — install deps + the package (uv only, no pip/requirements.txt)
8
+ - `PAYLOAD='{"source": ..., "name": ..., "audio_url": ...}' uv run audioclassifier --detection examples/configs/ads.toon` — process one audio file (needs `OPENAI_API_KEY`, `ffmpeg`)
9
+ - `uv run pytest` — unit tests; `-m integration` for the full-pipeline run (see tests/README.md)
10
+ - CLI logs go to `audioclassifier.log`, not the console; library imports are side-effect free (NullHandler logger)
11
+
12
+ ## Architecture (src/audioclassifier/)
13
+
14
+ - `__init__.py` — public API: `process_audio(...)`, `train_text_classifier()`
15
+ - `cli.py` — CLI entry point; `--detection <name|path>` selects a detection config
16
+ - `processing/` — download, transcribe (faster-whisper), cut (soundfile/numpy)
17
+ - `detection/` — LLM verification via pydantic-ai (`LLM_MODEL`, any provider), self-distilled text classifier
18
+ - `config/` — constants, detection config loader
19
+ - `cloud/` — all cloud integrations live here, nothing cloud-touching outside it (today: optional S3 upload of outputs via `storage=`)
20
+ - No bundled classifier: users supply a `.toon` config (names resolve from `./configs/`) or pass instructions/keywords directly; examples in `examples/configs/`
21
+ - `tests/unit/` mirrors this layout (`tests/unit/cloud/test_storage.py` ↔ `cloud/storage.py`)
22
+
23
+ ## Code Style
24
+
25
+ - Comments only when necessary, and concise — no narrating what the code already says
26
+ - Simple implementations over clever ones; no complexity for its own sake
27
+ - Standard Python conventions; `_`-prefix for module-internal functions and state
28
+ - Segregate by concern into packages — cloud code under `cloud/`, detection under `detection/`, audio work under `processing/`
@@ -0,0 +1,36 @@
1
+ FROM nvidia/cuda:12.0.0-base-ubuntu22.04
2
+
3
+ # Set environment variables to prevent user interaction during package installation
4
+ ENV DEBIAN_FRONTEND=noninteractive
5
+
6
+ # Install FFmpeg and system libraries (Python itself is managed by uv)
7
+ RUN apt-get update && \
8
+ apt-get install -y \
9
+ ffmpeg \
10
+ build-essential \
11
+ libavcodec-extra \
12
+ vim \
13
+ iputils-ping && \
14
+ apt-get clean && \
15
+ rm -rf /var/lib/apt/lists/*
16
+
17
+ COPY --from=ghcr.io/astral-sh/uv:0.8 /uv /usr/local/bin/uv
18
+
19
+ WORKDIR /app
20
+
21
+ # Dependency layer: cached unless the lockfile or python version changes
22
+ COPY pyproject.toml uv.lock .python-version /app/
23
+ RUN uv sync --frozen --no-dev --no-install-project
24
+
25
+ # Pre-download Whisper model during build (before src so code changes don't invalidate it)
26
+ RUN /app/.venv/bin/python -c "from faster_whisper import WhisperModel; import os; os.makedirs('/app/local_models/tiny', exist_ok=True); WhisperModel('tiny', download_root='/app/local_models/tiny')"
27
+
28
+ COPY src/ /app/src
29
+ COPY examples/configs/ /app/configs
30
+ RUN uv sync --frozen --no-dev
31
+
32
+
33
+ # Let ctranslate2 find the pip-installed cuBLAS/cuDNN libraries
34
+ ENV LD_LIBRARY_PATH=/app/.venv/lib/python3.13/site-packages/nvidia/cublas/lib:/app/.venv/lib/python3.13/site-packages/nvidia/cudnn/lib:$LD_LIBRARY_PATH
35
+
36
+ CMD ["/app/.venv/bin/audioclassifier"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Jon Fox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.5
2
+ Name: audio-classifier-tool
3
+ Version: 1.2.0
4
+ Summary: Detect and cut target segments (ads by default) from audio, config-driven and LLM-verified
5
+ Project-URL: Homepage, https://github.com/jon-fox/audio-classifier-tool
6
+ Project-URL: Repository, https://github.com/jon-fox/audio-classifier-tool
7
+ Author: Jon Fox
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ad-removal,ads,audio,classifier,podcast,whisper
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Multimedia :: Sound/Audio
15
+ Requires-Python: >=3.13
16
+ Requires-Dist: backoff>=2.2.1
17
+ Requires-Dist: boto3>=1.40.21
18
+ Requires-Dist: ctranslate2==4.8.2
19
+ Requires-Dist: faster-whisper==1.2.1
20
+ Requires-Dist: numpy>=2
21
+ Requires-Dist: nvidia-cublas-cu12; sys_platform == 'linux'
22
+ Requires-Dist: nvidia-cudnn-cu12==9.*; sys_platform == 'linux'
23
+ Requires-Dist: openai>=2
24
+ Requires-Dist: pydantic-ai-slim[openai]>=2
25
+ Requires-Dist: pydantic>=2.11.7
26
+ Requires-Dist: python-toon>=0.1.3
27
+ Requires-Dist: requests>=2.32.5
28
+ Requires-Dist: scikit-learn>=1.9.1
29
+ Requires-Dist: soundfile>=0.14.0
30
+ Description-Content-Type: text/markdown
31
+
32
+ # AudioClassifier - Dynamic Classifier used for Audio Segment Identification and Removal
33
+
34
+ Runs locally by default: one audio file in, filtered audio out. Needs an OpenAI API key and `ffmpeg`.
35
+
36
+ ## Quick Start
37
+
38
+ ```bash
39
+ uv sync
40
+ export OPENAI_API_KEY=<your-key>
41
+
42
+ PAYLOAD='{"source": "My Show", "name": "Episode 1", "audio_url": "<mp3-url>"}' \
43
+ uv run audioclassifier --detection examples/configs/ads.toon
44
+ ```
45
+
46
+ Results land in `output/<source>/<name>/`: the original mp3, the cleaned `*_filtered.mp3`, and a `transcripts/` dir with what was transcribed and each LLM cut/keep decision (with reasoning). Or with Docker:
47
+
48
+ ```bash
49
+ docker build -t audioclassifier-app .
50
+ docker run --gpus all \
51
+ -e OPENAI_API_KEY=<your-key> \
52
+ -e DETECTION_CONFIG=ads \
53
+ -e PAYLOAD='{"source": "My Show", "name": "Episode 1", "audio_url": "<mp3-url>"}' \
54
+ -v "$(pwd)/output:/app/output" \
55
+ audioclassifier-app
56
+ ```
57
+
58
+ Drop `--gpus all` to run on CPU (slower). The same container runs on any GPU box — RunPod, Modal, a gaming PC.
59
+
60
+ ## As a Library
61
+
62
+ ```bash
63
+ uv add audio-classifier-tool # pip install audio-classifier-tool
64
+ ```
65
+
66
+ ```python
67
+ import audioclassifier
68
+
69
+ result = audioclassifier.process_audio(
70
+ source="My Show",
71
+ name="Episode 1",
72
+ audio_url="<mp3-url>",
73
+ detection="examples/configs/ads.toon", # a .toon path or a name in ./configs
74
+ detection_instructions="...", # or pass the prompt directly
75
+ detection_keywords=["use code", ...], # and the keyword gate
76
+ )
77
+ print(result["output_path"], result["seconds_removed"])
78
+ ```
79
+
80
+ Importing has no side effects; configure the `"audioclassifier"` logger to see progress.
81
+
82
+ For a real end-to-end run with live console output and a decision summary:
83
+
84
+ ```bash
85
+ uv run python examples/process_audio.py "<mp3-url>"
86
+ ```
87
+
88
+ ## How It Works
89
+
90
+ - Downloads the audio from the `PAYLOAD` JSON (`source`, `name`, `audio_url`, optional `description`)
91
+ - Transcribes with faster-whisper (GPU-accelerated, CPU works too)
92
+ - Detects target segments with OpenAI — ads by default
93
+ - Cuts them and re-assembles the audio
94
+
95
+ ## Detection
96
+
97
+ The classifier is fully yours to define — nothing is bundled. A [TOON](https://github.com/toon-format/spec) config supplies the keywords and prompts: select one with `--detection <name|path>` or `DETECTION_CONFIG` (names resolve from `./configs/`), or pass the prompt and keywords directly (`detection_instructions`/`detection_keywords` in the API, `DETECTION_INSTRUCTIONS`/`DETECTION_KEYWORDS` env vars). Complete examples live in `examples/configs/` (ads, politics).
98
+
99
+ ## Text Classifier (optional, opt-in)
100
+
101
+ A self-distilled local classifier can add a third detection signal alongside keywords and audio analysis. Every processed audio file writes LLM-labeled decisions under `output/**/transcripts/` — that corpus is the classifier's training data, and it grows with each run.
102
+
103
+ Enable with `USE_TEXT_CLASSIFIER=true`: the classifier's flagged ranges join the LLM prompt (advisory only — the LLM still decides), and it retrains automatically after each run from the **full accumulated history** (retraining is from scratch, in seconds, so nothing is ever forgotten). Refresh manually anytime:
104
+
105
+ ```bash
106
+ uv run python examples/train_classifier.py # or audioclassifier.train_text_classifier()
107
+ ```
108
+
109
+ How it propagates: the durable memory is the decision files in `output/` — the model file (`local_models/text_classifier.joblib`) is a disposable cache rebuilt from them. Editing a decision file's `cut_ranges_seconds` after listening feeds your correction into the next training pass. In Docker, mount `local_models/` alongside `output/` to carry the model between containers (the data already survives via the `output/` mount).
110
+
111
+ ## Parallelism
112
+
113
+ One audio file per `process_audio` call. Multiple processes are safe, even in the same directory — downloads and segment audio live in per-run temp dirs, and the classifier model is written atomically. Within one process, run audio files sequentially (Whisper models load once and are reused); concurrent runs in threads are supported only with one shared detection config. Don't feed the same audio to two processes at once — they'd write the same output files.
114
+
115
+ ## Options
116
+
117
+ - `LLM_MODEL` — any [pydantic-ai model string](https://ai.pydantic.dev/models/) (default `openai:gpt-5.6`; e.g. `openai:gpt-5-nano` for cheapest, `anthropic:claude-sonnet-4-6`, `ollama:qwen3` — non-OpenAI providers may need their extra installed)
118
+ - `DISCORD_ALERTS=true DISCORD_WEBHOOK_URL=<url>` — processing alerts in Discord
119
+ - `storage="s3://bucket/prefix"` (API) or `"storage"` in `PAYLOAD` — upload the run's outputs (audio + transcripts + decisions) to S3 under `<prefix>/<source>/<name>/`, using ambient AWS credentials
120
+
121
+ ## License
122
+
123
+ [MIT](LICENSE)
@@ -0,0 +1,92 @@
1
+ # AudioClassifier - Dynamic Classifier used for Audio Segment Identification and Removal
2
+
3
+ Runs locally by default: one audio file in, filtered audio out. Needs an OpenAI API key and `ffmpeg`.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ uv sync
9
+ export OPENAI_API_KEY=<your-key>
10
+
11
+ PAYLOAD='{"source": "My Show", "name": "Episode 1", "audio_url": "<mp3-url>"}' \
12
+ uv run audioclassifier --detection examples/configs/ads.toon
13
+ ```
14
+
15
+ Results land in `output/<source>/<name>/`: the original mp3, the cleaned `*_filtered.mp3`, and a `transcripts/` dir with what was transcribed and each LLM cut/keep decision (with reasoning). Or with Docker:
16
+
17
+ ```bash
18
+ docker build -t audioclassifier-app .
19
+ docker run --gpus all \
20
+ -e OPENAI_API_KEY=<your-key> \
21
+ -e DETECTION_CONFIG=ads \
22
+ -e PAYLOAD='{"source": "My Show", "name": "Episode 1", "audio_url": "<mp3-url>"}' \
23
+ -v "$(pwd)/output:/app/output" \
24
+ audioclassifier-app
25
+ ```
26
+
27
+ Drop `--gpus all` to run on CPU (slower). The same container runs on any GPU box — RunPod, Modal, a gaming PC.
28
+
29
+ ## As a Library
30
+
31
+ ```bash
32
+ uv add audio-classifier-tool # pip install audio-classifier-tool
33
+ ```
34
+
35
+ ```python
36
+ import audioclassifier
37
+
38
+ result = audioclassifier.process_audio(
39
+ source="My Show",
40
+ name="Episode 1",
41
+ audio_url="<mp3-url>",
42
+ detection="examples/configs/ads.toon", # a .toon path or a name in ./configs
43
+ detection_instructions="...", # or pass the prompt directly
44
+ detection_keywords=["use code", ...], # and the keyword gate
45
+ )
46
+ print(result["output_path"], result["seconds_removed"])
47
+ ```
48
+
49
+ Importing has no side effects; configure the `"audioclassifier"` logger to see progress.
50
+
51
+ For a real end-to-end run with live console output and a decision summary:
52
+
53
+ ```bash
54
+ uv run python examples/process_audio.py "<mp3-url>"
55
+ ```
56
+
57
+ ## How It Works
58
+
59
+ - Downloads the audio from the `PAYLOAD` JSON (`source`, `name`, `audio_url`, optional `description`)
60
+ - Transcribes with faster-whisper (GPU-accelerated, CPU works too)
61
+ - Detects target segments with OpenAI — ads by default
62
+ - Cuts them and re-assembles the audio
63
+
64
+ ## Detection
65
+
66
+ The classifier is fully yours to define — nothing is bundled. A [TOON](https://github.com/toon-format/spec) config supplies the keywords and prompts: select one with `--detection <name|path>` or `DETECTION_CONFIG` (names resolve from `./configs/`), or pass the prompt and keywords directly (`detection_instructions`/`detection_keywords` in the API, `DETECTION_INSTRUCTIONS`/`DETECTION_KEYWORDS` env vars). Complete examples live in `examples/configs/` (ads, politics).
67
+
68
+ ## Text Classifier (optional, opt-in)
69
+
70
+ A self-distilled local classifier can add a third detection signal alongside keywords and audio analysis. Every processed audio file writes LLM-labeled decisions under `output/**/transcripts/` — that corpus is the classifier's training data, and it grows with each run.
71
+
72
+ Enable with `USE_TEXT_CLASSIFIER=true`: the classifier's flagged ranges join the LLM prompt (advisory only — the LLM still decides), and it retrains automatically after each run from the **full accumulated history** (retraining is from scratch, in seconds, so nothing is ever forgotten). Refresh manually anytime:
73
+
74
+ ```bash
75
+ uv run python examples/train_classifier.py # or audioclassifier.train_text_classifier()
76
+ ```
77
+
78
+ How it propagates: the durable memory is the decision files in `output/` — the model file (`local_models/text_classifier.joblib`) is a disposable cache rebuilt from them. Editing a decision file's `cut_ranges_seconds` after listening feeds your correction into the next training pass. In Docker, mount `local_models/` alongside `output/` to carry the model between containers (the data already survives via the `output/` mount).
79
+
80
+ ## Parallelism
81
+
82
+ One audio file per `process_audio` call. Multiple processes are safe, even in the same directory — downloads and segment audio live in per-run temp dirs, and the classifier model is written atomically. Within one process, run audio files sequentially (Whisper models load once and are reused); concurrent runs in threads are supported only with one shared detection config. Don't feed the same audio to two processes at once — they'd write the same output files.
83
+
84
+ ## Options
85
+
86
+ - `LLM_MODEL` — any [pydantic-ai model string](https://ai.pydantic.dev/models/) (default `openai:gpt-5.6`; e.g. `openai:gpt-5-nano` for cheapest, `anthropic:claude-sonnet-4-6`, `ollama:qwen3` — non-OpenAI providers may need their extra installed)
87
+ - `DISCORD_ALERTS=true DISCORD_WEBHOOK_URL=<url>` — processing alerts in Discord
88
+ - `storage="s3://bucket/prefix"` (API) or `"storage"` in `PAYLOAD` — upload the run's outputs (audio + transcripts + decisions) to S3 under `<prefix>/<source>/<name>/`, using ambient AWS credentials
89
+
90
+ ## License
91
+
92
+ [MIT](LICENSE)
@@ -0,0 +1,9 @@
1
+ services:
2
+ audioclassifier:
3
+ build: .
4
+ environment:
5
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
6
+ - PAYLOAD=${PAYLOAD}
7
+ - DETECTION_CONFIG=${DETECTION_CONFIG:-ads}
8
+ volumes:
9
+ - ./output:/app/output
@@ -0,0 +1,6 @@
1
+ name: ads
2
+ keywords[207]: signing up,use the code,support the show,use code,this episode is brought to you by,this show is brought to you by,"support for \\w+ comes from","i've been using \\w+",supplies are limited,"and enter code \\w+ at checkout",brought to you,this episode is,sponsors,sponsor,click the link in the description to find out more,sponsored by,advertisement,"visit [\\w-]+\\.com to save",use the promo code,"visit [-\\w.]+ to learn more",sponsoring,limited time,download the app,paid for by,"get \\d+% off your","save \\d+% on your","\\d+% discount on your","get \\d+% off",take a moment to thank our sponsor,purchase,sale,checkout,special offer,discount,discount code,promo code,promo,code,deal,offer,subscription service that,exclusive offer,"limited[-\\s]?time deal","limited[-\\s]?time offer","limited[-\\s]?time discount","limited[-\\s]?time sale",partnering,partner,partnered,promotion,link in the episode description,highly recommend,you have to try,shop,shop now,exclusive deal,affiliate link,commission earned,as an affiliate,partner program,affiliate disclosure,brought to you in part by,our friends at,a quick word from our sponsors,"listener[-\\s]?supported",thanks to our sponsor,subscribe today,try it for free,sign up now,don't miss out,order now,learn more,click here,visit now,explore more,read more,get your first month free,free trial,no obligation,"money[-\\s]?back guarantee",best price,partnered with,in collaboration with,powered by,endorsed by,brought to you by our partners,"(visit|check\\s(out|us\\sat|our\\swebsite)|go\\sto)\\s[\\w-]+(\\.[a-z]{2,})",act now,offer valid until,use our code,check the link below,click to learn more,brought to you in partnership with,save big,big savings,limited stock,early bird offer,new customers only,join now,exclusive for listeners,refer a friend,referral bonus,sign up for exclusive perks,try it today,as seen on,number one choice,voted best by,receive your,guaranteed results,award winning,customer favorite,see why everyone loves,start your journey,get access now,unbeatable value,get started today,your exclusive chance,contact us for more,fast delivery,don't delay,best in class,industry leading,on sale now,your satisfaction guaranteed,bet now,place your bets,gamble responsibly,must be 18 or older,download the draftkings app,odds boost,promo odds,parlay insurance,nicotine pouches,vape pods,tobacco-free nicotine,switch to vaping,smokeless alternative,juul compatible,telehealth visit,online doctor,rx delivered,consult a licensed physician,clinically proven results,fda cleared,lab-tested,doctor-formulated,ashwagandha gummies,adaptogen blend,hormone balancing,bedroom confidence,boost libido,feminine care,testosterone support,sexual wellness,lasting longer,increase stamina,intimate oil,male enhancement,natural male enhancement,anti-aging serum,retinol cream,collagen peptides,hair thickening,hair-loss solution,dermatologist recommended,clean beauty,spf moisturizer,meal kit,fresh ingredients delivered,chef-curated recipes,coffee subscription,wine club,snack box,first box free,robo-advisor,cryptocurrency exchange,commission-free trading,investing app,refinance your loan,credit repair,cash-back card,vpn service,password manager,cloud backup,identity theft protection,malware scan,data breach monitoring,secure browser,coding bootcamp,online mba,certificate program,career coaching,learn to code,masterclass,course bundle,discount flights,hotel deals,vacation package,airport transfer,ride credit,e-bike subscription,donate today,matching gift,join the movement,support our mission,paid for by the committee,grassroots campaign
3
+ prompts:
4
+ assistant_instructions: "Your role is to analyze text or files for advertisements. Prioritize accuracy and ensure all responses are concise and well-structured.\nWhen provided with specific scoring or timestamping instructions, follow them carefully."
5
+ sponsor_instructions: "Please review the following podcast\ndescription and extract only the names of sponsors, advertisers,\ncompanies, or organizations mentioned. Exclude any other details, links, or additional context.\nProvide just the names.\n\nExample Output:\nSponsors: [Company A, Company B]"
6
+ detection_instructions: "On a scale of 1-100, evaluate the confidence that the attached text contains an advertisement or institutional promotion.\n\n ### Guidelines for Detection:\n\n #### Handling Fragmented Content:\n - Evaluate individual text segments. For segments that appear incomplete or ambiguous, include the preceding and following segments within a **10-20 second window** to ensure the full ad is captured.\n - Prioritize removing content if:\n - It contains explicit ad indicators, such as mentions of sponsors, products, or services.\n - It includes calls to action, promotional language, or website/promo code references.\n - It is acceptable to capture some non-adjacent content as long as it ensures the entire ad is removed.\n - Avoid including unrelated segments that clearly lack ad indicators or disrupt the flow of detection.\n - Use the larger window only for content likely to span multiple segments (e.g., longer ad reads or storytelling formats).\n\n #### General Indicators:\n - Mentions of organizations, sponsors, or products, explicitly or indirectly.\n - Promotion of a service, product, subscription, or institution.\n - Highlighting unique benefits, features, or incentives, such as cost savings or exclusivity.\n - Encouraging listeners to trust or engage with a brand.\n - Contextual framing: Introducing a problem or need before recommending a solution.\n\n #### Institutional Promotions:\n - Promotion of an organization's **reputation, values, or societal contributions** rather than a specific product or service.\n - Examples include:\n - A corporation highlighting its environmental efforts (e.g., \"BP is committed to sustainability and a greener future\").\n - A university promoting its brand or academic excellence (e.g., \"Georgia Tech is a leader in innovation and research\").\n - Government agencies, NGOs, or advocacy groups promoting awareness or community engagement (e.g., \"Support our mission to fight climate change\").\n - Indicators of institutional promotions:\n - Statements reinforcing credibility, leadership, or legacy (e.g., \"A trusted name for over 100 years\").\n - Public relations-driven messaging emphasizing goodwill or societal impact.\n - Invitations to explore the organization's work rather than purchase a product (e.g., \"Learn more about our mission\").\n\n ##### Gambling and Betting:\n - Mentions of betting platforms, casinos, or wagering apps.\n - Use of odds, disclaimers, or age restrictions (e.g., \"Must be 21 or older\").\n - Phrases like \"risk-free bets,\" \"lock in your picks,\" or \"bet responsibly.\"\n\n ##### Tobacco, Nicotine, and Vaping:\n - Mentions of e-cigarettes, vaping pods, nicotine pouches, or smokeless products.\n - Framing products as harm reduction or \"cleaner alternatives.\"\n - Phrases like \"quit smoking the smart way,\" \"nicotine without the smoke.\"\n\n ##### Pharmaceuticals and Health Claims:\n - Promotion of prescription or over-the-counter drugs, supplements, or treatments.\n - Health claims (e.g., \"boost immunity,\" \"clinically proven,\" \"doctor recommended\").\n - References to FDA approval or regulatory compliance.\n\n ##### Adult Content and Intimate Wellness Products:\n - Promotion of dating platforms, adult content, or intimacy-related services.\n - **For men**:\n - Phrases like \"natural male enhancement,\" \"improve performance,\" \"testosterone boosters.\"\n - **For women**:\n - Terms like \"intimate wellness,\" \"boost libido,\" \"feminine rejuvenation,\" \"feel sexier.\"\n - Euphemistic or pseudo-medical language around intimacy, confidence, or bedroom health.\n - Includes supplements, devices, oils, and therapies marketed for sexual benefit or appeal.\n\n ##### Gender-Targeted Lifestyle and Beauty Promotions:\n - Skincare, makeup, cosmetics, or beauty product promotions.\n - Phrases like \"glowing skin,\" \"age-defying,\" \"get your best look.\"\n - Hair care, waxing, nail, or personal grooming products.\n - Phrases like \"salon-quality at home,\" \"confidence starts with your hair.\"\n - Self-care and empowerment framing.\n - \"You deserve it,\" \"treat yourself,\" \"upgrade your routine.\"\n - Weight loss or body image-focused products (e.g., slimming teas, detox kits, body sculpting).\n - \"Flatten your stomach,\" \"get your summer body,\" \"shed stubborn weight.\"\n\n #### Common Podcast Ad Categories\n Flag content that includes brand mentions, promo codes, or clear calls to action in these frequent podcast-ad verticals:\n - **Finance & Investing** - credit cards, trading apps, robo-advisors (\"get $10 in free BTC\").\n - **Tech & Cybersecurity** - VPNs, password managers, cloud backup (\"try it free for 30 days\").\n - **Food & Beverage Delivery** - meal kits, snack boxes, coffee/wine clubs (\"use code PODCAST\").\n - **Health & Wellness Services** - telehealth, therapy platforms, fitness apps, at-home lab tests.\n - **DTC Home & Lifestyle** - mattresses, bedding, furniture-in-a-box, home security.\n - **Beauty & Grooming** - razor clubs, hair-loss treatments, skincare subscriptions.\n - **Education & Career** - online courses, coding bootcamps, certificate programs.\n - **Pet Products** - pet-food subscriptions, tele-vet services, training apps.\n - **Entertainment & Media** - audiobooks, streaming services, ticket platforms.\n - **Travel & Mobility** - airlines, vacation bundles, rental cars, micro-mobility.\n - **Charitable & Political Appeals** - nonprofit fundraising, ballot-initiative promotions.\n\n Look for discount language, free trials, urgency (\"sign up today\"), or problem-solution framing to confirm promotional intent.\n\n #### Calls to Action:\n - Language prompting actions like:\n - Visiting a website or using a promo code (e.g., \"Use code PODCAST for 20% off\").\n - Signing up, downloading, subscribing, or purchasing.\n - Exploring or engaging with a mission, values, or achievements (e.g., \"Explore our impact.\").\n\n #### Distinctive Features:\n - Polished delivery styles (e.g., rehearsed tone, slogans, or taglines).\n - Emphasis on specific benefits or features.\n - Changes in tone, speed, or phrasing signaling promotional content.\n - Problem-solution narratives leading to product recommendations.\n\n #### Common Promotional Elements:\n - Mentions of discounts, limited-time offers, or urgency (e.g., \"Save now,\" \"Exclusive to listeners\").\n - Encouragement to act immediately (e.g., \"Don't wait, act now\").\n - References to solving a problem or enhancing a listener's experience.\n\n {optional_sponsors_section}\n\n ### Scoring and Timestamping:\n - Assign confidence scores as follows:\n - Above 60: Strong ad indicators (e.g., sponsor mentions, calls to action, promo codes).\n - 40-60: Content in the 40-60 range may include partial ad-like phrases but lacks a clear call to action or sponsorship mention.\n - Below 40: No clear ad indicators.\n - Ads often run for 30-60 seconds, but timestamps should match detected promotional content rather than assume a set length.\n - If multiple ad or promotional segments are detected, provide timestamps for each segment individually. However, if an ad is fragmented across a longer conversational segment, merge timestamps where necessary to capture the full promotional message without splitting it unnaturally.\n - For conversational-style ads that blend with organic content, focus on identifying the entire promotional context rather than isolating individual phrases. Ensure that subtle sponsorship mentions or integrated endorsements are fully captured.\n\n Output:\n Be concise, providing only the confidence score and the timestamps for each detected ad or promotional segment.\n\n Return only a valid JSON object with no additional text, explanations, or formatting. The response must strictly follow this format:\n {\n \"confidence_score\": 85,\n \"timestamps\": [\n {\"start\": 0.00, \"end\": 30.00},\n {\"start\": 45.00, \"end\": 75.00},\n {\"start\": 120.00, \"end\": 150.00}\n ]\n }"
@@ -0,0 +1,5 @@
1
+ name: politics
2
+ keywords[10]: election,senator,congress,campaign,ballot,legislation,president,governor,policy debate,partisan
3
+ prompts:
4
+ assistant_instructions: Your role is to analyze podcast transcripts for political content. Prioritize accuracy and follow the scoring and timestamping instructions carefully.
5
+ detection_instructions: "On a scale of 1-100, evaluate the confidence that the attached transcript contains political discussion (campaigns, elections, legislation, partisan commentary).\n\n{optional_sponsors_section}\n\nReport each distinct political segment as its own timestamp range; never span unrelated content between two segments. Return the confidence score and the timestamps for each detected segment."
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env python
2
+ """Run the real pipeline against an audio URL using the library API.
3
+
4
+ export OPENAI_API_KEY=<your-key>
5
+ uv run python examples/process_audio.py "<mp3-url>" \
6
+ --source "My Show" --name "Episode 1"
7
+
8
+ The classifier is config-driven: --detection selects what gets found and cut.
9
+ This example defaults to the ad classifier at examples/configs/ads.toon;
10
+ examples/configs/politics.toon shows a different classifier — pass it with:
11
+
12
+ --detection examples/configs/politics.toon
13
+
14
+ Progress streams to the console; results and per-segment LLM decisions are
15
+ printed at the end.
16
+ """
17
+
18
+ import argparse
19
+ import glob
20
+ import json
21
+ import logging
22
+ import os
23
+
24
+ import audioclassifier
25
+
26
+
27
+ def main():
28
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
29
+ parser.add_argument("audio_url", help="audio file url")
30
+ parser.add_argument("--source", default="Example Source")
31
+ parser.add_argument("--name", default="Example Audio")
32
+ parser.add_argument(
33
+ "--detection",
34
+ default=os.path.join(os.path.dirname(__file__), "configs", "ads.toon"),
35
+ help="detection config .toon path or a name in ./configs "
36
+ "(default: the example ad classifier)",
37
+ )
38
+ args = parser.parse_args()
39
+
40
+ logger = logging.getLogger("audioclassifier")
41
+ handler = logging.StreamHandler()
42
+ handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
43
+ logger.addHandler(handler)
44
+ logger.setLevel(logging.INFO)
45
+
46
+ print(f"Detection config: {args.detection}")
47
+ result = audioclassifier.process_audio(
48
+ source=args.source,
49
+ name=args.name,
50
+ audio_url=args.audio_url,
51
+ detection=args.detection,
52
+ )
53
+
54
+ print("\n=== Result ===")
55
+ print(f"Output: {result['output_path']}")
56
+ print(f"Original duration: {result['original_duration']:.0f}s")
57
+ print(f"Filtered duration: {result['filtered_duration']:.0f}s")
58
+ print(f"Removed: {result['seconds_removed']:.0f}s")
59
+
60
+ transcripts_dir = os.path.join(
61
+ os.path.dirname(result["output_path"]), "transcripts"
62
+ )
63
+ for path in sorted(glob.glob(os.path.join(transcripts_dir, "*_decision.json"))):
64
+ decision = json.load(open(path))
65
+ print(f"\n{os.path.basename(path)}: {decision['action'].upper()}")
66
+ if decision["cut_ranges_seconds"]:
67
+ print(f" ranges: {decision['cut_ranges_seconds']}")
68
+ print(f" reasoning: {decision['reasoning']}")
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python
2
+ """Train the self-distilled ad classifier from past runs' decision files.
3
+
4
+ uv run python examples/train_classifier.py [output-dir]
5
+
6
+ Every processed audio file adds training data (transcripts labeled by the LLM's
7
+ cut/keep decisions under output/). Once trained, runs automatically feed the
8
+ classifier's flags to the LLM as an extra detection signal.
9
+ """
10
+
11
+ import sys
12
+
13
+ import audioclassifier
14
+
15
+
16
+ def main():
17
+ output_dir = sys.argv[1] if len(sys.argv) > 1 else None
18
+ metrics = audioclassifier.train_text_classifier(output_dir)
19
+ print("Trained:")
20
+ for key, value in metrics.items():
21
+ print(f" {key}: {value}")
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "audio-classifier-tool"
3
+ version = "1.2.0"
4
+ description = "Detect and cut target segments (ads by default) from audio, config-driven and LLM-verified"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.13"
8
+ authors = [{ name = "Jon Fox" }]
9
+ keywords = ["audio", "podcast", "ads", "ad-removal", "whisper", "classifier"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "Programming Language :: Python :: 3.13",
14
+ "Topic :: Multimedia :: Sound/Audio",
15
+ ]
16
+ dependencies = [
17
+ "backoff>=2.2.1",
18
+ "boto3>=1.40.21",
19
+ "ctranslate2==4.8.2",
20
+ "faster-whisper==1.2.1",
21
+ "numpy>=2",
22
+ # cuBLAS + cuDNN 9 for ctranslate2 GPU inference (previously bundled via torch/apt)
23
+ "nvidia-cublas-cu12; sys_platform == 'linux'",
24
+ "nvidia-cudnn-cu12==9.*; sys_platform == 'linux'",
25
+ "openai>=2",
26
+ "pydantic>=2.11.7",
27
+ "pydantic-ai-slim[openai]>=2",
28
+ "python-toon>=0.1.3",
29
+ "requests>=2.32.5",
30
+ "scikit-learn>=1.9.1",
31
+ "soundfile>=0.14.0",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/jon-fox/audio-classifier-tool"
36
+ Repository = "https://github.com/jon-fox/audio-classifier-tool"
37
+
38
+ [project.scripts]
39
+ audioclassifier = "audioclassifier.cli:main"
40
+
41
+ [tool.pytest.ini_options]
42
+ markers = ["integration: full pipeline run; needs OPENAI_API_KEY and network"]
43
+ addopts = "-m 'not integration'"
44
+
45
+ [build-system]
46
+ requires = ["hatchling"]
47
+ build-backend = "hatchling.build"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/audioclassifier"]
51
+
52
+ [dependency-groups]
53
+ dev = [
54
+ "black>=26.5.1",
55
+ "pytest>=9.1.1",
56
+ ]
@@ -0,0 +1,59 @@
1
+ """AudioClassifier: detect and cut target segments from audio (ads by default)."""
2
+
3
+
4
+ def process_audio(
5
+ source,
6
+ name,
7
+ audio_url,
8
+ detection=None,
9
+ detection_instructions=None,
10
+ detection_keywords=None,
11
+ description=None,
12
+ storage=None,
13
+ ):
14
+ """Process one audio file: download, transcribe, detect, and cut.
15
+
16
+ source groups outputs (output/<source>/<name>/); name is used for the
17
+ output filenames. detection: a config name (in ./configs) or a .toon path;
18
+ detection_instructions / detection_keywords override the selected config
19
+ directly. description is optional context about the audio for the
20
+ detection config's context-extraction prompt. storage: optional
21
+ s3://bucket/prefix — outputs are uploaded under <prefix>/<source>/<name>/.
22
+ Returns a dict with output_path, filtered_duration, original_duration,
23
+ seconds_removed, and (with storage) the uploaded S3 URIs.
24
+
25
+ Configure the "audioclassifier" logger to see progress; set OPENAI_API_KEY
26
+ (or the provider key matching LLM_MODEL) before calling.
27
+ """
28
+ if detection or detection_instructions or detection_keywords:
29
+ from audioclassifier.config.detection_config import set_detection_config
30
+
31
+ set_detection_config(
32
+ detection,
33
+ instructions=detection_instructions,
34
+ keywords=detection_keywords,
35
+ )
36
+
37
+ from audioclassifier.cli import process_payload
38
+
39
+ return process_payload(
40
+ {
41
+ "source": source,
42
+ "name": name,
43
+ "audio_url": audio_url,
44
+ "description": description,
45
+ "storage": storage,
46
+ }
47
+ )
48
+
49
+
50
+ def train_text_classifier(output_dir=None):
51
+ """Train the self-distilled ad classifier from past runs' decision files.
52
+
53
+ Returns training metrics. The trained model is picked up automatically by
54
+ subsequent runs as an extra detection signal.
55
+ """
56
+ from audioclassifier.config.constants import FINISHED_MP3_DIR
57
+ from audioclassifier.detection import text_classifier
58
+
59
+ return text_classifier.train(output_dir or FINISHED_MP3_DIR)