shadow-tools 0.7.1__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.1 → shadow_tools-0.7.2}/PKG-INFO +4 -1
  2. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/README.md +3 -0
  3. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/pyproject.toml +1 -1
  4. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/__init__.py +1 -1
  5. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/generator.py +18 -6
  6. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/.gitignore +0 -0
  7. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/LICENSE +0 -0
  8. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/ai.py +0 -0
  9. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/async_tools.py +0 -0
  10. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/bpe.py +0 -0
  11. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/cli.py +0 -0
  12. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/config.py +0 -0
  13. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/data.py +0 -0
  14. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/files.py +0 -0
  15. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/hashing.py +0 -0
  16. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/http.py +0 -0
  17. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/ids.py +0 -0
  18. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/network.py +0 -0
  19. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/retry.py +0 -0
  20. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/security.py +0 -0
  21. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/text.py +0 -0
  22. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/training.py +0 -0
  23. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/validation.py +0 -0
  24. {shadow_tools-0.7.1 → shadow_tools-0.7.2}/src/shadowtools/websocket.py +0 -0
  25. {shadow_tools-0.7.1 → 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.1
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
@@ -73,6 +73,9 @@ print(say(bot, "The future of technology", max_tokens=80))
73
73
  bot.save("my_text_model")
74
74
  ```
75
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
+
76
79
  Install it with `pip install "shadow-tools[ml,nlp]"`. The package trains the
77
80
  tokenizer and Transformer together, then converts predicted token IDs back to
78
81
  normal text for you.
@@ -45,6 +45,9 @@ print(say(bot, "The future of technology", max_tokens=80))
45
45
  bot.save("my_text_model")
46
46
  ```
47
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
+
48
51
  Install it with `pip install "shadow-tools[ml,nlp]"`. The package trains the
49
52
  tokenizer and Transformer together, then converts predicted token IDs back to
50
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.1"
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"
@@ -15,7 +15,7 @@ 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.1"
18
+ __version__ = "0.7.2"
19
19
 
20
20
  __all__ = [
21
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",
@@ -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,11 +191,12 @@ 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
 
@@ -194,7 +205,8 @@ def generate_text(generator: TextGenerator, prompt: str, **options: Any) -> str:
194
205
  return generator.generate(prompt, **options)
195
206
 
196
207
 
197
- def learn(source: str | Path | Sequence[str], *, epochs: int = 10, **options: Any) -> TextGenerator:
208
+ def learn(source: str | Path | Sequence[str], *, epochs: int = 10,
209
+ verbose: bool = True, **options: Any) -> TextGenerator:
198
210
  """Beginner-friendly training: give it a text file, text, or list of texts."""
199
211
  text: str | Sequence[str]
200
212
  if isinstance(source, Path):
@@ -207,7 +219,7 @@ def learn(source: str | Path | Sequence[str], *, epochs: int = 10, **options: An
207
219
  text = source
208
220
  else:
209
221
  text = source
210
- generator, _ = train_generator(text, epochs=epochs, **options)
222
+ generator, _ = train_generator(text, epochs=epochs, verbose=verbose, **options)
211
223
  return generator
212
224
 
213
225
 
File without changes
File without changes