modotte 1.0.0__py3-none-any.whl

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.
modotte/AIRealNet.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point: ``from modotte.AIRealNet import Audio, Image``."""
2
+
3
+ from .audio import Audio
4
+ from .image import Image
5
+
6
+ __all__ = ["Audio", "Image"]
modotte/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .AIRealNet import Audio, Image
2
+
3
+ __all__ = ["Audio", "Image"]
4
+ __version__ = "1.0.0"
modotte/_common.py ADDED
@@ -0,0 +1,108 @@
1
+ """Shared helpers: model directory lookup, download and pipeline loading."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import threading
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional, Tuple, cast
9
+
10
+ import torch
11
+
12
+ _lock = threading.Lock()
13
+ _cache: Dict[Tuple[str, str], Tuple[Any, str]] = {}
14
+
15
+
16
+ def home_dir() -> Path:
17
+ try:
18
+ return Path.home()
19
+ except (RuntimeError, KeyError):
20
+ return Path(os.path.expanduser("~"))
21
+
22
+
23
+ def model_exists(path: Path) -> bool:
24
+ return path.is_dir() and (path / "config.json").is_file()
25
+
26
+
27
+ def detect_device() -> str:
28
+ return "cuda" if torch.cuda.is_available() else "cpu"
29
+
30
+
31
+ def resolve_hf_token(hf_token: Optional[str] = None) -> Optional[str]:
32
+ """Return the Hugging Face token to use, or ``None`` if there isn't one.
33
+
34
+ Order of precedence:
35
+ 1. the ``hf_token`` argument passed by the caller,
36
+ 2. the ``HF_TOKEN`` (or legacy ``HUGGING_FACE_HUB_TOKEN``) environment
37
+ variable, or a token saved with ``hf auth login``
38
+ (both handled by ``huggingface_hub.get_token``).
39
+ """
40
+ if hf_token is not None:
41
+ if not isinstance(hf_token, str):
42
+ raise TypeError(
43
+ f"hf_token must be a string, got {type(hf_token).__name__}."
44
+ )
45
+ hf_token = hf_token.strip()
46
+ if hf_token:
47
+ return hf_token
48
+
49
+ from huggingface_hub import get_token
50
+
51
+ return get_token() or None
52
+
53
+
54
+ def download_model(
55
+ repo_id: str, model_dir: Path, hf_token: Optional[str] = None
56
+ ) -> None:
57
+ from huggingface_hub import snapshot_download
58
+
59
+ token = resolve_hf_token(hf_token)
60
+ if token is None:
61
+ print(
62
+ "[modotte] Tip: no Hugging Face token found. Set the HF_TOKEN "
63
+ "environment variable or pass hf_token=\"hf_...\" for higher rate "
64
+ "limits and faster downloads "
65
+ "(create one at https://huggingface.co/settings/tokens)."
66
+ )
67
+
68
+ model_dir.mkdir(parents=True, exist_ok=True)
69
+ print(f"[modotte] Model not found. Downloading {repo_id} to {model_dir} ...")
70
+ snapshot_download(repo_id=repo_id, local_dir=str(model_dir), token=token)
71
+
72
+
73
+ def load_pipeline(
74
+ task: str,
75
+ repo_id: str,
76
+ model_dir: Path,
77
+ hf_token: Optional[str] = None,
78
+ **pipeline_kwargs,
79
+ ):
80
+ """Return (pipeline, device); download the model first if it is missing.
81
+
82
+ ``hf_token`` is only used for the download (the model is loaded from the
83
+ local ``model_dir``). Pipelines are cached per (task, model_dir), so the
84
+ check/download/load only happens the first time a class is initialized.
85
+ """
86
+ key = (task, str(model_dir))
87
+ with _lock:
88
+ if key in _cache:
89
+ return _cache[key]
90
+
91
+ if not model_exists(model_dir):
92
+ download_model(repo_id, model_dir, hf_token)
93
+ if not model_exists(model_dir):
94
+ raise RuntimeError(
95
+ f"Download finished but no model was found in {model_dir}."
96
+ )
97
+
98
+ from transformers import pipeline
99
+
100
+ device = detect_device()
101
+ print(f"[modotte] Using device: {device}")
102
+ # `pipeline` is typed with one overload per literal task name, so a
103
+ # runtime `task` string can't match; cast to Any for the type checker.
104
+ pipe = cast(Any, pipeline)(
105
+ task, model=str(model_dir), device=device, **pipeline_kwargs
106
+ )
107
+ _cache[key] = (pipe, device)
108
+ return _cache[key]
modotte/_formats.py ADDED
@@ -0,0 +1,180 @@
1
+ """File-format support: extension checks and audio/image decoding.
2
+
3
+ Audio: MP3, WAV, AAC, FLAC, OGG, Opus, M4A, AIFF
4
+ Image: JPG, JPEG, PNG, WebP, GIF, SVG, BMP, TIFF, AVIF, HEIC, HEIF
5
+
6
+ Heavy optional decoders (soundfile, PyAV, cairosvg, pillow-heif) are imported
7
+ lazily so that only the formats you actually use need them.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import io
13
+ import os
14
+ from pathlib import Path
15
+ from typing import Tuple, Union
16
+
17
+ import numpy as np
18
+
19
+ PathLike = Union[str, os.PathLike]
20
+
21
+ AUDIO_EXTENSIONS: Tuple[str, ...] = (
22
+ ".mp3", ".wav", ".aac", ".flac", ".ogg", ".opus", ".m4a", ".aiff", ".aif",
23
+ )
24
+ IMAGE_EXTENSIONS: Tuple[str, ...] = (
25
+ ".jpg", ".jpeg", ".png", ".webp", ".gif", ".svg", ".bmp",
26
+ ".tif", ".tiff", ".avif", ".heic", ".heif",
27
+ )
28
+
29
+ AUDIO_FORMATS = "MP3, WAV, AAC, FLAC, OGG, Opus, M4A, AIFF"
30
+ IMAGE_FORMATS = "JPG, JPEG, PNG, WebP, GIF, SVG, BMP, TIFF, AVIF, HEIC, HEIF"
31
+
32
+ SVG_RENDER_WIDTH = 512
33
+
34
+
35
+ def check_extension(path: PathLike, allowed: Tuple[str, ...], kind: str, formats: str) -> str:
36
+ ext = Path(path).suffix.lower()
37
+ if ext not in allowed:
38
+ raise ValueError(
39
+ f"Unsupported {kind} format '{ext or '(no extension)'}'. "
40
+ f"Supported formats: {formats}."
41
+ )
42
+ return ext
43
+
44
+
45
+ # ----------------------------------------------------------------------
46
+ # Audio
47
+ # ----------------------------------------------------------------------
48
+ def _decode_with_av(path: PathLike, target_sr: int) -> np.ndarray:
49
+ """Decode any ffmpeg-supported audio (AAC, M4A, ...) to mono float32."""
50
+ import av
51
+
52
+ container = av.open(str(path))
53
+ try:
54
+ stream = next((s for s in container.streams if s.type == "audio"), None)
55
+ if stream is None:
56
+ raise ValueError("No audio stream found in file.")
57
+
58
+ resampler = av.AudioResampler(format="flt", layout="mono", rate=target_sr)
59
+ parts = []
60
+
61
+ def collect(frames):
62
+ if frames is None:
63
+ return
64
+ if not isinstance(frames, (list, tuple)):
65
+ frames = [frames]
66
+ for f in frames:
67
+ parts.append(f.to_ndarray().reshape(-1))
68
+
69
+ for frame in container.decode(stream):
70
+ collect(resampler.resample(frame)) # type: ignore
71
+ collect(resampler.resample(None)) # flush
72
+ finally:
73
+ container.close()
74
+
75
+ if not parts:
76
+ return np.zeros(0, dtype=np.float32)
77
+ return np.concatenate(parts).astype(np.float32)
78
+
79
+
80
+ def read_audio(path: PathLike, target_sr: int) -> np.ndarray:
81
+ """Read an audio file as mono float32 at ``target_sr``."""
82
+ check_extension(path, AUDIO_EXTENSIONS, "audio", AUDIO_FORMATS)
83
+
84
+ sf_error = None
85
+ try:
86
+ import soundfile as sf
87
+ from scipy.signal import resample_poly
88
+
89
+ audio, sr = sf.read(str(path), always_2d=False)
90
+ audio = np.asarray(audio, dtype=np.float32)
91
+ if audio.ndim == 2:
92
+ audio = audio.mean(axis=1)
93
+ if sr != target_sr:
94
+ audio = resample_poly(audio, target_sr, sr).astype(np.float32)
95
+ return audio
96
+ except Exception as exc: # AAC/M4A (and some MP3 builds) land here
97
+ sf_error = exc
98
+
99
+ try:
100
+ return _decode_with_av(path, target_sr)
101
+ except ImportError as exc:
102
+ raise RuntimeError(
103
+ f"Could not decode '{Path(path).name}' with soundfile ({sf_error}) and "
104
+ f"PyAV is not installed. Install it with: pip install av"
105
+ ) from exc
106
+ except Exception as exc:
107
+ raise RuntimeError(
108
+ f"Could not decode '{Path(path).name}': soundfile error: {sf_error}; "
109
+ f"PyAV error: {exc}"
110
+ ) from exc
111
+
112
+
113
+ # ----------------------------------------------------------------------
114
+ # Images
115
+ # ----------------------------------------------------------------------
116
+ _plugins_registered = False
117
+
118
+
119
+ def _register_image_plugins() -> None:
120
+ """Enable HEIC/HEIF/AVIF in Pillow when the plugins are installed."""
121
+ global _plugins_registered
122
+ if _plugins_registered:
123
+ return
124
+ _plugins_registered = True
125
+ try:
126
+ from pillow_heif import register_heif_opener
127
+
128
+ register_heif_opener()
129
+ except ImportError:
130
+ pass
131
+
132
+
133
+ def flatten_to_rgb(img):
134
+ """Convert to RGB, compositing transparency onto white."""
135
+ from PIL import Image as PILImage
136
+
137
+ has_alpha = img.mode in ("RGBA", "LA", "PA") or (
138
+ img.mode == "P" and "transparency" in img.info
139
+ )
140
+ if has_alpha:
141
+ rgba = img.convert("RGBA")
142
+ background = PILImage.new("RGB", rgba.size, (255, 255, 255))
143
+ background.paste(rgba, mask=rgba.getchannel("A"))
144
+ return background
145
+ return img.convert("RGB")
146
+
147
+
148
+ def read_image(path: PathLike):
149
+ """Open an image file of any supported format as an RGB PIL image."""
150
+ from PIL import Image as PILImage
151
+ from PIL import ImageOps
152
+
153
+ ext = check_extension(path, IMAGE_EXTENSIONS, "image", IMAGE_FORMATS)
154
+
155
+ if ext == ".svg":
156
+ try:
157
+ import cairosvg
158
+ except ImportError as exc:
159
+ raise RuntimeError(
160
+ "SVG support requires cairosvg. Install it with: pip install cairosvg"
161
+ ) from exc
162
+ png = cairosvg.svg2png(
163
+ url=str(path), output_width=SVG_RENDER_WIDTH, background_color="white"
164
+ )
165
+ with PILImage.open(io.BytesIO(png)) as img: # type: ignore
166
+ return flatten_to_rgb(img)
167
+
168
+ _register_image_plugins()
169
+ try:
170
+ with PILImage.open(path) as img:
171
+ img.seek(0) # first frame of GIF / multi-page TIFF
172
+ img = ImageOps.exif_transpose(img) # honour camera orientation
173
+ return flatten_to_rgb(img)
174
+ except PILImage.UnidentifiedImageError as exc:
175
+ hint = ""
176
+ if ext in (".heic", ".heif"):
177
+ hint = " Install HEIC/HEIF support with: pip install pillow-heif"
178
+ elif ext == ".avif":
179
+ hint = " Install AVIF support with: pip install pillow-heif (or Pillow >= 11.3)"
180
+ raise RuntimeError(f"Could not read image '{Path(path).name}'.{hint}") from exc
modotte/audio.py ADDED
@@ -0,0 +1,182 @@
1
+ """Audio classification: AI-generated vs. human voice."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional, Tuple, Union
8
+
9
+ import numpy as np
10
+
11
+ from ._common import home_dir, load_pipeline
12
+ from ._formats import AUDIO_EXTENSIONS, AUDIO_FORMATS, read_audio
13
+
14
+ MODEL_ID = "Modotte/AIRealNet-Audio"
15
+ MODEL_DIR_NAME = "AIRealNetAudio"
16
+ SAMPLE_RATE = 16_000
17
+ CHUNK_SECONDS = 12
18
+ CHUNK_SAMPLES = SAMPLE_RATE * CHUNK_SECONDS
19
+ DEFAULT_THRESHOLD = 0.50
20
+
21
+ __all__ = ["Audio"]
22
+
23
+
24
+ def _validate_threshold(threshold: Any) -> float:
25
+ """Threshold must be a float in the range 0.0 to 1.0 (inclusive)."""
26
+ if isinstance(threshold, bool) or not isinstance(threshold, (float, np.floating)):
27
+ raise TypeError(
28
+ f"threshold must be a float between 0.0 and 1.0, "
29
+ f"got {type(threshold).__name__}."
30
+ )
31
+ value = float(threshold)
32
+ if not (0.0 <= value <= 1.0): # also rejects NaN
33
+ raise ValueError(f"threshold must be between 0.0 and 1.0, got {value}.")
34
+ return value
35
+
36
+
37
+ class Audio:
38
+ """Audio classifier that scores speech as AI-generated or human.
39
+
40
+ On first initialization ``~/AIRealNetAudio`` is checked; if the model is
41
+ missing it is downloaded there. Runs on CUDA if available, else CPU.
42
+
43
+ ``hf_token`` (optional) is a Hugging Face access token used for the model
44
+ download, giving higher rate limits and faster downloads. If omitted, the
45
+ ``HF_TOKEN`` environment variable (or a token saved via ``hf auth login``)
46
+ is used when available.
47
+ """
48
+
49
+ SUPPORTED_FORMATS = AUDIO_FORMATS
50
+ SUPPORTED_EXTENSIONS = AUDIO_EXTENSIONS
51
+
52
+ def __init__(
53
+ self,
54
+ model_dir: Optional[Union[str, os.PathLike]] = None,
55
+ hf_token: Optional[str] = None,
56
+ ):
57
+ self.model_dir = Path(model_dir) if model_dir else home_dir() / MODEL_DIR_NAME
58
+ self._pipeline, self.device = load_pipeline(
59
+ "audio-classification",
60
+ MODEL_ID,
61
+ self.model_dir,
62
+ hf_token=hf_token,
63
+ trust_remote_code=True,
64
+ )
65
+
66
+ @staticmethod
67
+ def load_audio(path: Union[str, os.PathLike]) -> np.ndarray:
68
+ audio = read_audio(path, SAMPLE_RATE)
69
+
70
+ peak = float(np.max(np.abs(audio))) if audio.size else 0.0
71
+ if peak > 1.0:
72
+ audio = audio / peak
73
+
74
+ return audio
75
+
76
+ @staticmethod
77
+ def chunk_audio(audio: np.ndarray) -> List[np.ndarray]:
78
+ if len(audio) == 0:
79
+ raise ValueError("The audio file is empty.")
80
+
81
+ chunks = []
82
+ for start in range(0, len(audio), CHUNK_SAMPLES):
83
+ chunk = audio[start:start + CHUNK_SAMPLES]
84
+ # The model is trained on 12-second chunks; pad the last one.
85
+ if len(chunk) < CHUNK_SAMPLES:
86
+ chunk = np.pad(chunk, (0, CHUNK_SAMPLES - len(chunk)), mode="constant")
87
+ chunks.append(chunk.astype(np.float32))
88
+ return chunks
89
+
90
+ @staticmethod
91
+ def get_label_scores(result: List[Dict[str, Any]]) -> Tuple[float, float]:
92
+ ai = 0.0
93
+ human = 0.0
94
+
95
+ for item in result:
96
+ label = str(item["label"]).lower()
97
+ score = float(item["score"])
98
+
99
+ if (
100
+ label in {"aivoice", "ai", "fake", "spoof", "ai-generated"}
101
+ or "ai" in label
102
+ or "spoof" in label
103
+ ):
104
+ ai = score
105
+ elif (
106
+ label in {"humanvoice", "human", "real"}
107
+ or "human" in label
108
+ or "real" in label
109
+ ):
110
+ human = score
111
+
112
+ # Fallback for unexpected model labels.
113
+ if ai == 0.0 and human == 0.0 and result:
114
+ top = max(result, key=lambda x: x["score"])
115
+ top_label = str(top["label"]).lower()
116
+ if "ai" in top_label or "fake" in top_label or "spoof" in top_label:
117
+ ai = float(top["score"])
118
+ human = 1.0 - ai
119
+ else:
120
+ human = float(top["score"])
121
+ ai = 1.0 - human
122
+
123
+ return ai, human
124
+
125
+ def predict(
126
+ self,
127
+ path: Union[str, os.PathLike],
128
+ threshold: float = DEFAULT_THRESHOLD,
129
+ ) -> Dict[str, Any]:
130
+ """Classify an audio file.
131
+
132
+ Supported formats: MP3, WAV, AAC, FLAC, OGG, Opus, M4A, AIFF.
133
+
134
+ ``threshold`` must be a float from 0.0 to 1.0 (inclusive); other
135
+ types raise ``TypeError`` and out-of-range values raise ``ValueError``.
136
+
137
+ Returns ``label`` ("AI" or "Human"), ``is_ai``, ``confidence``,
138
+ ``ai_score``, ``human_score``, ``duration``, ``threshold``,
139
+ ``device`` and per-chunk ``chunks`` details.
140
+ """
141
+ threshold = _validate_threshold(threshold)
142
+
143
+ if not path:
144
+ raise ValueError("Please provide an audio file path.")
145
+ if not Path(path).is_file():
146
+ raise FileNotFoundError(f"Audio file not found: {path}")
147
+
148
+ audio = self.load_audio(path)
149
+ chunks = self.chunk_audio(audio)
150
+
151
+ ai_scores, human_scores, rows = [], [], []
152
+ for idx, chunk in enumerate(chunks, start=1):
153
+ result = self._pipeline({"array": chunk, "sampling_rate": SAMPLE_RATE})
154
+ ai, human = self.get_label_scores(result)
155
+ ai_scores.append(ai)
156
+ human_scores.append(human)
157
+ rows.append(
158
+ {
159
+ "chunk": idx,
160
+ "start": (idx - 1) * CHUNK_SECONDS,
161
+ "ai_score": round(ai, 4),
162
+ "human_score": round(human, 4),
163
+ }
164
+ )
165
+
166
+ ai_score = float(np.mean(ai_scores))
167
+ human_score = float(np.mean(human_scores))
168
+ is_ai = ai_score >= threshold
169
+
170
+ return {
171
+ "label": "AI" if is_ai else "Human",
172
+ "is_ai": is_ai,
173
+ "confidence": ai_score if is_ai else human_score,
174
+ "ai_score": ai_score,
175
+ "human_score": human_score,
176
+ "duration": len(audio) / SAMPLE_RATE,
177
+ "threshold": threshold,
178
+ "device": self.device,
179
+ "chunks": rows,
180
+ }
181
+
182
+ __call__ = predict
modotte/image.py ADDED
@@ -0,0 +1,83 @@
1
+ """Image classification: AI-generated vs. real photographs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any, Dict, Optional, Union
8
+
9
+ import numpy as np
10
+ from PIL import Image as PILImage
11
+
12
+ from ._common import home_dir, load_pipeline
13
+ from ._formats import IMAGE_EXTENSIONS, IMAGE_FORMATS, flatten_to_rgb, read_image
14
+
15
+ MODEL_ID = "Modotte/AIRealNet"
16
+ MODEL_DIR_NAME = "AIRealNetImage"
17
+
18
+ ImageInput = Union[str, os.PathLike, PILImage.Image, np.ndarray]
19
+
20
+
21
+ class Image:
22
+ """AIRealNet (SwinV2 Tiny) binary image classifier.
23
+
24
+ Class 0: AI-generated image ("artificial")
25
+ Class 1: Real human image ("real")
26
+
27
+ On first initialization ``~/AIRealNetImage`` is checked; if the model is
28
+ missing it is downloaded there. Runs on CUDA if available, else CPU.
29
+
30
+ ``hf_token`` (optional) is a Hugging Face access token used for the model
31
+ download, giving higher rate limits and faster downloads. If omitted, the
32
+ ``HF_TOKEN`` environment variable (or a token saved via ``hf auth login``)
33
+ is used when available.
34
+ """
35
+
36
+ SUPPORTED_FORMATS = IMAGE_FORMATS
37
+ SUPPORTED_EXTENSIONS = IMAGE_EXTENSIONS
38
+
39
+ def __init__(
40
+ self,
41
+ model_dir: Optional[Union[str, os.PathLike]] = None,
42
+ hf_token: Optional[str] = None,
43
+ ):
44
+ self.model_dir = Path(model_dir) if model_dir else home_dir() / MODEL_DIR_NAME
45
+ self._pipeline, self.device = load_pipeline(
46
+ "image-classification", MODEL_ID, self.model_dir, hf_token=hf_token
47
+ )
48
+
49
+ @staticmethod
50
+ def _to_pil(image: ImageInput) -> PILImage.Image:
51
+ if isinstance(image, PILImage.Image):
52
+ return flatten_to_rgb(image)
53
+ if isinstance(image, np.ndarray):
54
+ return flatten_to_rgb(PILImage.fromarray(image))
55
+ if not Path(image).is_file():
56
+ raise FileNotFoundError(f"Image file not found: {image}")
57
+ return read_image(image)
58
+
59
+ def predict(self, image: ImageInput) -> Dict[str, Any]:
60
+ """Classify an image (path, PIL image or numpy array).
61
+
62
+ Supported files: JPG, JPEG, PNG, WebP, GIF, SVG, BMP, TIFF, AVIF, HEIC, HEIF.
63
+
64
+ Returns ``label`` (top class, e.g. "artificial" or "real"),
65
+ ``confidence``, ``is_ai``, and ``scores`` such as
66
+ ``{'artificial': 0.94, 'real': 0.06}``.
67
+ """
68
+ if image is None:
69
+ raise ValueError("Please provide an image.")
70
+
71
+ results = self._pipeline(self._to_pil(image))
72
+ scores = {r["label"]: round(float(r["score"]), 3) for r in results}
73
+ label = max(results, key=lambda r: r["score"])["label"]
74
+
75
+ return {
76
+ "label": label,
77
+ "confidence": scores[label],
78
+ "is_ai": label.lower() in {"artificial", "ai", "fake"} or "artificial" in label.lower(),
79
+ "scores": scores,
80
+ "device": self.device,
81
+ }
82
+
83
+ __call__ = predict
@@ -0,0 +1,360 @@
1
+ Metadata-Version: 2.4
2
+ Name: modotte
3
+ Version: 1.0.0
4
+ Summary: A universal Python package for accessing and using all AI models trained by Modotte.
5
+ Author: Sujal Rajpoot, Parvesh Rawal
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 modotte-python
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Repository, https://github.com/Modotte/modotte-python
29
+ Project-URL: Issues, https://github.com/Modotte/modotte-python/issues
30
+ Keywords: ai,artificial-intelligence,machine-learning,deep-learning,models,modotte,pytorch,transformers
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Programming Language :: Python :: 3 :: Only
35
+ Classifier: Programming Language :: Python :: 3.9
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
41
+ Requires-Python: >=3.9
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE
44
+ Requires-Dist: numpy
45
+ Requires-Dist: scipy
46
+ Requires-Dist: soundfile
47
+ Requires-Dist: pillow>=10.0
48
+ Requires-Dist: pillow-heif
49
+ Requires-Dist: cairosvg
50
+ Requires-Dist: av
51
+ Requires-Dist: torch
52
+ Requires-Dist: transformers
53
+ Requires-Dist: huggingface_hub
54
+ Dynamic: license-file
55
+
56
+ # modotte-python
57
+
58
+ A universal Python package for accessing and using all AI models trained by Modotte.
59
+
60
+ ## Installation
61
+
62
+ ### Install from PyPI
63
+
64
+ Install the latest stable version directly from PyPI:
65
+
66
+ ```bash
67
+ pip install modotte
68
+ ```
69
+
70
+ Then import Modotte in your Python project:
71
+
72
+ ```python
73
+ from modotte import Audio, Image
74
+ ```
75
+
76
+ ### Upgrade to the latest version
77
+
78
+ ```bash
79
+ pip install --upgrade modotte
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Install from GitHub
85
+
86
+ You can also install the latest development version directly from GitHub:
87
+
88
+ ```bash
89
+ pip install git+https://github.com/Modotte/modotte-python.git
90
+ ```
91
+
92
+ ### Clone the repository
93
+
94
+ If you want to develop or modify Modotte locally:
95
+
96
+ ```bash
97
+ git clone https://github.com/Modotte/modotte-python.git
98
+ cd modotte-python
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Project Structure
104
+
105
+ ```text
106
+ modotte-python/
107
+ ├── 📄 .gitignore
108
+ ├── 📄 LICENSE
109
+ ├── 📄 README.md
110
+ ├── 📄 pyproject.toml
111
+ └── 📁 src/
112
+ └── 📁 modotte/
113
+ ├── 📄 AIRealNet.py
114
+ ├── 📄 __init__.py
115
+ ├── 📄 _common.py ← internal
116
+ ├── 📄 _formats.py ← internal
117
+ ├── 📄 audio.py ← public API
118
+ └── 📄 image.py ← public API
119
+ ```
120
+
121
+ ## Hugging Face Token (Optional, Recommended)
122
+
123
+ The models are downloaded from the Hugging Face Hub on first use.
124
+
125
+ Without a token, you may see:
126
+
127
+ ```text
128
+ Warning: You are sending unauthenticated requests to the HF Hub
129
+ ```
130
+
131
+ Unauthenticated requests are subject to lower rate limits. A free Hugging Face token can provide higher rate limits and more reliable downloads.
132
+
133
+ Create a token from:
134
+
135
+ https://huggingface.co/settings/tokens
136
+
137
+ There are three ways to provide it.
138
+
139
+ ### 1. Pass the token directly
140
+
141
+ ```python
142
+ from modotte import Audio, Image
143
+
144
+ detector = Audio(hf_token="hf_xxxxxxxxxxxxxxxx")
145
+ detector = Image(hf_token="hf_xxxxxxxxxxxxxxxx")
146
+ ```
147
+
148
+ ### 2. Set the `HF_TOKEN` environment variable
149
+
150
+ #### Windows PowerShell — current session
151
+
152
+ ```powershell
153
+ $env:HF_TOKEN = "hf_xxxxxxxxxxxxxxxx"
154
+ ```
155
+
156
+ #### Windows — permanent
157
+
158
+ ```powershell
159
+ setx HF_TOKEN "hf_xxxxxxxxxxxxxxxx"
160
+ ```
161
+
162
+ Open a new terminal after running `setx`.
163
+
164
+ #### macOS / Linux
165
+
166
+ ```bash
167
+ export HF_TOKEN="hf_xxxxxxxxxxxxxxxx"
168
+ ```
169
+
170
+ ### 3. Log in using the Hugging Face CLI
171
+
172
+ ```bash
173
+ hf auth login
174
+ ```
175
+
176
+ An explicit `hf_token=` argument takes priority over the `HF_TOKEN` environment variable.
177
+
178
+ The token is only used for downloading models. Modotte does not store or print your token.
179
+
180
+ Models are downloaded once and subsequently loaded from the local cache.
181
+
182
+ ---
183
+
184
+ ## Audio Classification
185
+
186
+ Import the audio detector:
187
+
188
+ ```python
189
+ from modotte import Audio
190
+
191
+ detector = Audio()
192
+ ```
193
+
194
+ On first use, Modotte checks for the model locally and downloads it if necessary.
195
+
196
+ You can optionally provide a Hugging Face token:
197
+
198
+ ```python
199
+ detector = Audio(hf_token="hf_xxxxxxxxxxxxxxxx")
200
+ ```
201
+
202
+ ### Detect Bonafide / Real Audio
203
+
204
+ ```python
205
+ result = detector.predict("bonafide.wav", threshold=0.5)
206
+
207
+ print(result["label"])
208
+ print(result["confidence"])
209
+ ```
210
+
211
+ ### Detect Spoof / AI-Generated Audio
212
+
213
+ ```python
214
+ result = detector.predict("spoof.wav", threshold=0.5)
215
+
216
+ print(result["label"])
217
+ print(result["confidence"])
218
+ ```
219
+
220
+ The `threshold` parameter must be a float between `0.0` and `1.0`, inclusive.
221
+
222
+ Invalid types such as integers, strings, or booleans raise `TypeError`.
223
+
224
+ Values outside the range `0.0`–`1.0` raise `ValueError`.
225
+
226
+ ---
227
+
228
+ ## Image Classification
229
+
230
+ Import the image detector:
231
+
232
+ ```python
233
+ from modotte import Image
234
+
235
+ detector = Image()
236
+ ```
237
+
238
+ On first use, Modotte checks for the model locally and downloads it if necessary.
239
+
240
+ You can optionally provide a Hugging Face token:
241
+
242
+ ```python
243
+ detector = Image(hf_token="hf_xxxxxxxxxxxxxxxx")
244
+ ```
245
+
246
+ Run a prediction:
247
+
248
+ ```python
249
+ result = detector.predict("cat.jpg")
250
+
251
+ print(result["label"])
252
+ print(result["confidence"])
253
+ print(result["scores"])
254
+ ```
255
+
256
+ Example:
257
+
258
+ ```python
259
+ {
260
+ "label": "real",
261
+ "confidence": 0.94,
262
+ "scores": {
263
+ "artificial": 0.06,
264
+ "real": 0.94
265
+ }
266
+ }
267
+ ```
268
+
269
+ `Modotte/AIRealNet` is a binary SwinV2 Tiny classifier:
270
+
271
+ * Class 0: AI-generated
272
+ * Class 1: Real human image
273
+
274
+ Both import styles are supported:
275
+
276
+ ```python
277
+ from modotte import Audio, Image
278
+ ```
279
+
280
+ and:
281
+
282
+ ```python
283
+ from modotte.AIRealNet import Audio, Image
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Supported file types
289
+
290
+ - **Audio:** MP3, WAV, AAC, FLAC, OGG, Opus, M4A, AIFF
291
+ - **Image:** JPG, JPEG, PNG, WebP, GIF, SVG, BMP, TIFF, AVIF, HEIC, HEIF
292
+
293
+ Other extensions raise `ValueError`. WAV/FLAC/OGG/Opus/AIFF/MP3 are decoded with
294
+ `soundfile`; AAC and M4A (and any file `soundfile` can't read) fall back to PyAV,
295
+ which bundles ffmpeg. HEIC/HEIF/AVIF need `pillow-heif`, and SVG needs `cairosvg`
296
+ (which requires the Cairo library on some systems). GIF and multi-page TIFF use
297
+ the first frame; transparent images are composited on white.
298
+
299
+ Models load on CUDA when available, otherwise CPU.
300
+
301
+ ---
302
+
303
+ ## Quick Start
304
+
305
+ After installing Modotte:
306
+
307
+ ```bash
308
+ pip install modotte
309
+ ```
310
+
311
+ You can immediately use the supported models:
312
+
313
+ ```python
314
+ from modotte import Audio, Image
315
+
316
+ audio_detector = Audio()
317
+ image_detector = Image()
318
+
319
+ audio_result = audio_detector.predict("audio.wav")
320
+ image_result = image_detector.predict("image.jpg")
321
+
322
+ print(audio_result)
323
+ print(image_result)
324
+ ```
325
+
326
+ ---
327
+
328
+ ## Development
329
+
330
+ Clone the repository:
331
+
332
+ ```bash
333
+ git clone https://github.com/Modotte/modotte-python.git
334
+ cd modotte-python
335
+ ```
336
+
337
+ Create a virtual environment:
338
+
339
+ ```bash
340
+ python -m venv .venv
341
+ ```
342
+
343
+ Activate it and install the package in editable mode:
344
+
345
+ ```bash
346
+ pip install -e .
347
+ ```
348
+
349
+ After making changes to the source code, the changes will be available immediately in your environment.
350
+
351
+ ---
352
+ ## Authors
353
+
354
+ - **Sujal Rajpoot** — [GitHub](https://github.com/sujalrajpoot)
355
+ - **Parvesh Rawal** — [GitHub](https://github.com/Parveshiiii)
356
+ ---
357
+
358
+ ## License
359
+
360
+ See the [LICENSE](LICENSE) file for licensing information.
@@ -0,0 +1,11 @@
1
+ modotte/AIRealNet.py,sha256=-EkQHhDe1vqUBOXYfcUvVAwh9rT3NpDQqO4sFPvo5Yg,148
2
+ modotte/__init__.py,sha256=M51bKNoTjHjX9ivnY_XkF-r-BmzwEkUIRUNp16Tfh5A,88
3
+ modotte/_common.py,sha256=0cR0QKEKD6-88dXj_70iTvd-hQnU6Fd2648XW7khO7s,3446
4
+ modotte/_formats.py,sha256=oM0eD6RthKO3sAx3tmTx52MCSPRta1gtGXNhXIkt34I,6038
5
+ modotte/audio.py,sha256=EIfVuw_xDKWBGKtVIRN40eg4h9o8nN3OsuX-m_r7fg4,6140
6
+ modotte/image.py,sha256=j9X1jFzGzu0JkOk8tvZF58ENs3J-X6pXs_hUbxIiTzE,2897
7
+ modotte-1.0.0.dist-info/licenses/LICENSE,sha256=MGQJ5agwuqNNKHU9qlSAhyV64mksKZRNoUlJ_MID5T8,1092
8
+ modotte-1.0.0.dist-info/METADATA,sha256=D_n-CDdMXlUUD4DJv9d4vxwGQwK4pt1aBYiu8-ef5-s,8754
9
+ modotte-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ modotte-1.0.0.dist-info/top_level.txt,sha256=sscqzIvtSNpW6yZ83-ym4_F4MpLARGG4P0yZuq0mM4E,8
11
+ modotte-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 modotte-python
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 @@
1
+ modotte