Skip to content

Commit fc4d4f1

Browse files
author
Nexus Cortex
committed
feat(broca2): Phase 1 — BPE tokenizer with training, encode/decode, UTF-8/Romanian support, CLI trainer, 12 tests passing
1 parent 66fa9b0 commit fc4d4f1

3 files changed

Lines changed: 1176 additions & 0 deletions

File tree

cmd/cortex-tokenizer/main.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"flag"
6+
"fmt"
7+
"os"
8+
"strings"
9+
"time"
10+
11+
"nexus-cortex/cortex"
12+
)
13+
14+
// ─────────────────────────────────────────────────────────────────────
15+
// cortex-tokenizer — CLI tool for training and testing BPE tokenizers
16+
// ─────────────────────────────────────────────────────────────────────
17+
//
18+
// Usage:
19+
// Train a new tokenizer:
20+
// go run cmd/cortex-tokenizer/main.go -train -input corpus.txt -vocab-size 32768 -output tokenizer.json
21+
//
22+
// Encode text with an existing tokenizer:
23+
// go run cmd/cortex-tokenizer/main.go -encode "Hello world" -tokenizer tokenizer.json
24+
//
25+
// Interactive mode:
26+
// go run cmd/cortex-tokenizer/main.go -interactive -tokenizer tokenizer.json
27+
28+
func main() {
29+
// Flags
30+
trainMode := flag.Bool("train", false, "Train a new tokenizer from corpus")
31+
inputFile := flag.String("input", "", "Input corpus file (one sentence per line)")
32+
outputFile := flag.String("output", "tokenizer.json", "Output tokenizer file path")
33+
vocabSize := flag.Int("vocab-size", 32768, "Target vocabulary size")
34+
tokenizerFile := flag.String("tokenizer", "", "Path to trained tokenizer for encode/decode")
35+
encodeText := flag.String("encode", "", "Text to encode")
36+
interactive := flag.Bool("interactive", false, "Interactive encode/decode mode")
37+
38+
flag.Parse()
39+
40+
if *trainMode {
41+
runTrain(*inputFile, *outputFile, *vocabSize)
42+
return
43+
}
44+
45+
if *encodeText != "" {
46+
if *tokenizerFile == "" {
47+
fmt.Fprintln(os.Stderr, "Error: -tokenizer required for encoding")
48+
os.Exit(1)
49+
}
50+
runEncode(*tokenizerFile, *encodeText)
51+
return
52+
}
53+
54+
if *interactive {
55+
if *tokenizerFile == "" {
56+
fmt.Fprintln(os.Stderr, "Error: -tokenizer required for interactive mode")
57+
os.Exit(1)
58+
}
59+
runInteractive(*tokenizerFile)
60+
return
61+
}
62+
63+
flag.Usage()
64+
}
65+
66+
func runTrain(inputFile, outputFile string, vocabSize int) {
67+
if inputFile == "" {
68+
fmt.Fprintln(os.Stderr, "Error: -input corpus file required for training")
69+
os.Exit(1)
70+
}
71+
72+
fmt.Printf("╔══════════════════════════════════════════════════════╗\n")
73+
fmt.Printf("║ Nexus Cortex — BPE Tokenizer Trainer ║\n")
74+
fmt.Printf("╚══════════════════════════════════════════════════════╝\n\n")
75+
76+
// Read corpus
77+
fmt.Printf("[1/3] Reading corpus from %s...\n", inputFile)
78+
lines, err := readLines(inputFile)
79+
if err != nil {
80+
fmt.Fprintf(os.Stderr, "Error reading corpus: %v\n", err)
81+
os.Exit(1)
82+
}
83+
fmt.Printf(" → %d lines loaded\n\n", len(lines))
84+
85+
// Train
86+
fmt.Printf("[2/3] Training BPE tokenizer (target vocab: %d)...\n", vocabSize)
87+
start := time.Now()
88+
89+
tok := cortex.NewBPETokenizer(vocabSize)
90+
tok.Train(lines)
91+
92+
elapsed := time.Since(start)
93+
fmt.Printf(" → Training completed in %s\n", elapsed.Round(time.Millisecond))
94+
fmt.Printf(" → Final vocab: %d tokens, %d merges\n\n", tok.ActualVocabSize(), len(tok.Merges))
95+
96+
// Save
97+
fmt.Printf("[3/3] Saving tokenizer to %s...\n", outputFile)
98+
if err := tok.Save(outputFile); err != nil {
99+
fmt.Fprintf(os.Stderr, "Error saving: %v\n", err)
100+
os.Exit(1)
101+
}
102+
103+
info, _ := os.Stat(outputFile)
104+
fmt.Printf(" → Saved (%d bytes)\n\n", info.Size())
105+
106+
// Quick test
107+
fmt.Println("── Quick Test ─────────────────────────────────────────")
108+
testTexts := []string{
109+
"Hello world",
110+
"the cat sat on the mat",
111+
}
112+
if len(lines) > 0 {
113+
// Use first line of corpus
114+
firstLine := strings.TrimSpace(lines[0])
115+
if len(firstLine) > 80 {
116+
firstLine = firstLine[:80]
117+
}
118+
testTexts = append(testTexts, firstLine)
119+
}
120+
121+
for _, text := range testTexts {
122+
ids := tok.Encode(text)
123+
tokens := tok.DecodeTokens(ids)
124+
decoded := tok.Decode(ids)
125+
fmt.Printf(" Input: %q\n", text)
126+
fmt.Printf(" Tokens: %v\n", tokens)
127+
fmt.Printf(" IDs: %v\n", ids)
128+
fmt.Printf(" Decoded: %q\n", decoded)
129+
if decoded == text {
130+
fmt.Println(" ✓ Roundtrip OK")
131+
} else {
132+
fmt.Println(" ✗ Roundtrip FAILED")
133+
}
134+
fmt.Println()
135+
}
136+
137+
fmt.Println("Done.")
138+
}
139+
140+
func runEncode(tokenizerFile, text string) {
141+
tok, err := cortex.LoadBPETokenizer(tokenizerFile)
142+
if err != nil {
143+
fmt.Fprintf(os.Stderr, "Error loading tokenizer: %v\n", err)
144+
os.Exit(1)
145+
}
146+
147+
ids := tok.Encode(text)
148+
tokens := tok.DecodeTokens(ids)
149+
decoded := tok.Decode(ids)
150+
151+
fmt.Printf("Input: %q\n", text)
152+
fmt.Printf("Token IDs: %v\n", ids)
153+
fmt.Printf("Tokens: %v\n", tokens)
154+
fmt.Printf("Decoded: %q\n", decoded)
155+
fmt.Printf("Count: %d tokens\n", len(ids))
156+
}
157+
158+
func runInteractive(tokenizerFile string) {
159+
tok, err := cortex.LoadBPETokenizer(tokenizerFile)
160+
if err != nil {
161+
fmt.Fprintf(os.Stderr, "Error loading tokenizer: %v\n", err)
162+
os.Exit(1)
163+
}
164+
165+
fmt.Printf("Nexus BPE Tokenizer — Interactive Mode\n")
166+
fmt.Printf("Vocab: %d tokens, %d merges\n", tok.ActualVocabSize(), len(tok.Merges))
167+
fmt.Printf("Type text to tokenize (Ctrl+C to exit):\n\n")
168+
169+
scanner := bufio.NewScanner(os.Stdin)
170+
for {
171+
fmt.Print("> ")
172+
if !scanner.Scan() {
173+
break
174+
}
175+
text := scanner.Text()
176+
if text == "" {
177+
continue
178+
}
179+
180+
ids := tok.Encode(text)
181+
tokens := tok.DecodeTokens(ids)
182+
decoded := tok.Decode(ids)
183+
184+
fmt.Printf(" Tokens: %v\n", tokens)
185+
fmt.Printf(" IDs: %v\n", ids)
186+
fmt.Printf(" Count: %d\n", len(ids))
187+
fmt.Printf(" Decoded: %q\n", decoded)
188+
if decoded == text {
189+
fmt.Println(" ✓ Roundtrip OK")
190+
} else {
191+
fmt.Println(" ✗ Roundtrip MISMATCH")
192+
}
193+
fmt.Println()
194+
}
195+
}
196+
197+
func readLines(path string) ([]string, error) {
198+
f, err := os.Open(path)
199+
if err != nil {
200+
return nil, err
201+
}
202+
defer f.Close()
203+
204+
var lines []string
205+
scanner := bufio.NewScanner(f)
206+
// Increase buffer for long lines
207+
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
208+
209+
for scanner.Scan() {
210+
line := strings.TrimSpace(scanner.Text())
211+
if line != "" {
212+
lines = append(lines, line)
213+
}
214+
}
215+
return lines, scanner.Err()
216+
}

0 commit comments

Comments
 (0)