shadow-tools 0.7.0__tar.gz → 0.7.2__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 (25) hide show
  1. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/PKG-INFO +8 -6
  2. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/README.md +7 -5
  3. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/pyproject.toml +1 -1
  4. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/__init__.py +4 -4
  5. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/generator.py +38 -4
  6. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/.gitignore +0 -0
  7. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/LICENSE +0 -0
  8. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/ai.py +0 -0
  9. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/async_tools.py +0 -0
  10. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/bpe.py +0 -0
  11. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/cli.py +0 -0
  12. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/config.py +0 -0
  13. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/data.py +0 -0
  14. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/files.py +0 -0
  15. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/hashing.py +0 -0
  16. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/http.py +0 -0
  17. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/ids.py +0 -0
  18. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/network.py +0 -0
  19. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/retry.py +0 -0
  20. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/security.py +0 -0
  21. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/text.py +0 -0
  22. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/training.py +0 -0
  23. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/validation.py +0 -0
  24. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/src/shadowtools/websocket.py +0 -0
  25. {shadow_tools-0.7.0 → shadow_tools-0.7.2}/tests/test_core.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: shadow-tools
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: A practical all-in-one Python toolkit for HTTP, WebSockets, files, data, validation, async work, and CLIs.
5
5
  Project-URL: Homepage, https://github.com/WilliamLaurento/shadow-tools
6
6
  Project-URL: Issues, https://github.com/WilliamLaurento/shadow-tools/issues
@@ -66,14 +66,16 @@ word as a separate thing. Install it with `pip install "shadow-tools[nlp]"`.
66
66
  ## Train and generate text from one package
67
67
 
68
68
  ```python
69
- from shadowtools import generate_text, train_generator
69
+ from shadowtools import learn, say
70
70
 
71
- corpus = open("corpus.txt", encoding="utf-8").read()
72
- model, history = train_generator(corpus, epochs=20)
73
- print(generate_text(model, "The future of technology", max_tokens=80))
74
- model.save("my_text_model")
71
+ bot = learn("corpus.txt", epochs=20)
72
+ print(say(bot, "The future of technology", max_tokens=80))
73
+ bot.save("my_text_model")
75
74
  ```
76
75
 
76
+ Training prints the tokenizer status plus each epoch and loss. Use
77
+ `learn("corpus.txt", epochs=20, verbose=False)` when you do not want updates.
78
+
77
79
  Install it with `pip install "shadow-tools[ml,nlp]"`. The package trains the
78
80
  tokenizer and Transformer together, then converts predicted token IDs back to
79
81
  normal text for you.
@@ -38,14 +38,16 @@ word as a separate thing. Install it with `pip install "shadow-tools[nlp]"`.
38
38
  ## Train and generate text from one package
39
39
 
40
40
  ```python
41
- from shadowtools import generate_text, train_generator
41
+ from shadowtools import learn, say
42
42
 
43
- corpus = open("corpus.txt", encoding="utf-8").read()
44
- model, history = train_generator(corpus, epochs=20)
45
- print(generate_text(model, "The future of technology", max_tokens=80))
46
- model.save("my_text_model")
43
+ bot = learn("corpus.txt", epochs=20)
44
+ print(say(bot, "The future of technology", max_tokens=80))
45
+ bot.save("my_text_model")
47
46
  ```
48
47
 
48
+ Training prints the tokenizer status plus each epoch and loss. Use
49
+ `learn("corpus.txt", epochs=20, verbose=False)` when you do not want updates.
50
+
49
51
  Install it with `pip install "shadow-tools[ml,nlp]"`. The package trains the
50
52
  tokenizer and Transformer together, then converts predicted token IDs back to
51
53
  normal text for you.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "shadow-tools"
7
- version = "0.7.0"
7
+ version = "0.7.2"
8
8
  description = "A practical all-in-one Python toolkit for HTTP, WebSockets, files, data, validation, async work, and CLIs."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -5,7 +5,7 @@ from .bpe import BPETokenizer
5
5
  from .config import env, require_env, to_bool
6
6
  from .files import append_text, ensure_dir, read_text, write_text
7
7
  from .hashing import file_hash, hash_text
8
- from .generator import GeneratorConfig, TextGenerator, generate_text, train_generator
8
+ from .generator import GeneratorConfig, TextGenerator, generate_text, learn, say, train_generator
9
9
  from .http import get, request
10
10
  from .ids import random_token, timestamp_id, uuid4
11
11
  from .network import generate_random_ips, generate_test_ips, is_subdomain_of, make_subdomain, resolve_dns, reverse_dns
@@ -15,10 +15,10 @@ from .text import chunks, slugify, to_camel, to_snake
15
15
  from .training import SmartTokenizer, fine_tune, make_loaders, predict_text, train, train_text
16
16
  from .validation import is_email, is_url, require_keys
17
17
 
18
- __version__ = "0.7.0"
18
+ __version__ = "0.7.2"
19
19
 
20
20
  __all__ = [
21
- "append_text", "audit_headers", "BPETokenizer", "chunks", "ensure_dir", "env", "file_hash", "GeneratorConfig", "generate_random_ips", "generate_test_ips", "generate_text", "get", "hash_text", "is_email", "is_subdomain_of", "is_url", "make_password", "make_subdomain", "random_token", "read_csv",
22
- "read_json", "read_text", "request", "require_env", "require_keys", "retry", "reverse_dns", "resolve_dns", "slugify", "timestamp_id", "to_bool", "to_camel", "to_snake", "TextGenerator", "train", "train_generator", "fine_tune", "make_loaders", "SmartTokenizer", "train_text", "predict_text", "uuid4", "write_csv",
21
+ "append_text", "audit_headers", "BPETokenizer", "chunks", "ensure_dir", "env", "file_hash", "GeneratorConfig", "generate_random_ips", "generate_test_ips", "generate_text", "get", "hash_text", "is_email", "is_subdomain_of", "is_url", "learn", "make_password", "make_subdomain", "random_token", "read_csv",
22
+ "read_json", "read_text", "request", "require_env", "require_keys", "retry", "reverse_dns", "resolve_dns", "say", "slugify", "timestamp_id", "to_bool", "to_camel", "to_snake", "TextGenerator", "train", "train_generator", "fine_tune", "make_loaders", "SmartTokenizer", "train_text", "predict_text", "uuid4", "write_csv",
23
23
  "write_json", "write_text", "redact_secrets", "verify_file_hash",
24
24
  ]
@@ -19,6 +19,7 @@ class GeneratorConfig:
19
19
  batch_size: int = 16
20
20
  learning_rate: float = 3e-4
21
21
  device: str = "auto"
22
+ verbose: bool = True
22
23
 
23
24
 
24
25
  class TextGenerator:
@@ -78,18 +79,24 @@ class TextGenerator:
78
79
 
79
80
  return CausalTransformer().to(self.device)
80
81
 
81
- def fit(self, texts: str | Sequence[str], *, epochs: int = 10) -> list[dict[str, float]]:
82
+ def fit(self, texts: str | Sequence[str], *, epochs: int = 10,
83
+ verbose: bool | None = None) -> list[dict[str, float]]:
82
84
  """Train from a large text string or a list of documents."""
83
85
  if epochs < 1:
84
86
  raise ValueError("epochs must be at least 1.")
85
87
  corpus = texts if isinstance(texts, str) else "\n".join(texts)
86
88
  if not isinstance(corpus, str) or len(corpus.strip()) < 100:
87
89
  raise ValueError("Give the generator at least 100 characters of training text.")
90
+ show_progress = self.config.verbose if verbose is None else verbose
91
+ if show_progress:
92
+ print("Building tokenizer...")
88
93
  self.tokenizer = BPETokenizer(vocab_size=self.config.vocab_size, min_frequency=1).train([corpus])
89
94
  token_ids = self.tokenizer.encode(corpus, add_special_tokens=False)["input_ids"]
90
95
  if len(token_ids) <= self.config.block_size:
91
96
  raise ValueError("Training text needs more tokens than block_size.")
92
97
  self.model = self._build_model(self.tokenizer.size)
98
+ if show_progress:
99
+ print(f"Tokenizer ready: {self.tokenizer.size} tokens. Training model for {epochs} epoch(s)...")
93
100
  torch = self.torch
94
101
  block_size = self.config.block_size
95
102
 
@@ -119,7 +126,10 @@ class TextGenerator:
119
126
  torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0)
120
127
  optimizer.step()
121
128
  total_loss += float(loss.detach().cpu())
122
- history.append({"epoch": float(epoch), "loss": total_loss / len(loader)})
129
+ metrics = {"epoch": float(epoch), "loss": total_loss / len(loader)}
130
+ history.append(metrics)
131
+ if show_progress:
132
+ print(f"Epoch {epoch}/{epochs} | loss: {metrics['loss']:.4f}")
123
133
  return history
124
134
 
125
135
  def generate(self, prompt: str, *, max_tokens: int = 80, temperature: float = 0.8,
@@ -181,14 +191,38 @@ def train_generator(texts: str | Sequence[str], *, epochs: int = 10,
181
191
  vocab_size: int = 4_000, block_size: int = 64,
182
192
  embedding_size: int = 192, heads: int = 6, layers: int = 4,
183
193
  batch_size: int = 16, learning_rate: float = 3e-4,
184
- device: str = "auto") -> tuple[TextGenerator, list[dict[str, float]]]:
194
+ device: str = "auto", verbose: bool = True) -> tuple[TextGenerator, list[dict[str, float]]]:
185
195
  """Train a text generator in one call, without writing model boilerplate."""
186
196
  generator = TextGenerator(GeneratorConfig(vocab_size=vocab_size, block_size=block_size,
187
197
  embedding_size=embedding_size, heads=heads, layers=layers,
188
- batch_size=batch_size, learning_rate=learning_rate, device=device))
198
+ batch_size=batch_size, learning_rate=learning_rate, device=device,
199
+ verbose=verbose))
189
200
  return generator, generator.fit(texts, epochs=epochs)
190
201
 
191
202
 
192
203
  def generate_text(generator: TextGenerator, prompt: str, **options: Any) -> str:
193
204
  """Short form for ``generator.generate(prompt)``."""
194
205
  return generator.generate(prompt, **options)
206
+
207
+
208
+ def learn(source: str | Path | Sequence[str], *, epochs: int = 10,
209
+ verbose: bool = True, **options: Any) -> TextGenerator:
210
+ """Beginner-friendly training: give it a text file, text, or list of texts."""
211
+ text: str | Sequence[str]
212
+ if isinstance(source, Path):
213
+ text = source.read_text(encoding="utf-8")
214
+ elif isinstance(source, str):
215
+ try:
216
+ candidate = Path(source)
217
+ text = candidate.read_text(encoding="utf-8") if candidate.is_file() else source
218
+ except OSError:
219
+ text = source
220
+ else:
221
+ text = source
222
+ generator, _ = train_generator(text, epochs=epochs, verbose=verbose, **options)
223
+ return generator
224
+
225
+
226
+ def say(generator: TextGenerator, prompt: str, **options: Any) -> str:
227
+ """Beginner-friendly name for generating text from a trained model."""
228
+ return generator.generate(prompt, **options)
File without changes
File without changes