a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
272 lines
7.0 KiB
Go
272 lines
7.0 KiB
Go
package embedding
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
// wordPieceTokenizer is a minimal, dependency-free implementation of the
|
|
// HuggingFace fast-tokenizer trio used by BERT-style vocabularies:
|
|
// BertNormalizer → BertPreTokenizer → WordPiece. It loads the standard
|
|
// tokenizer.json and reproduces the exact token-ID sequences of the
|
|
// reference implementation for that configuration (verified by golden
|
|
// tests against the upstream tokenizer).
|
|
//
|
|
// Scope: only the configuration the bundled static code-embedding model
|
|
// uses — BertNormalizer{clean_text, handle_chinese_chars, lowercase,
|
|
// strip_accents:null} + BertPreTokenizer + WordPiece{"##"} — is
|
|
// implemented. Loading a tokenizer.json with a different model type
|
|
// fails loudly rather than mis-tokenizing quietly.
|
|
type wordPieceTokenizer struct {
|
|
vocab map[string]int
|
|
unkID int
|
|
contPrefix string
|
|
maxWordChars int
|
|
lowercase bool
|
|
stripAccents bool
|
|
cleanText bool
|
|
handleCJK bool
|
|
}
|
|
|
|
// tokenizerJSON mirrors just the fields of tokenizer.json this
|
|
// implementation consumes.
|
|
type tokenizerJSON struct {
|
|
Normalizer *struct {
|
|
Type string `json:"type"`
|
|
CleanText *bool `json:"clean_text"`
|
|
HandleCJK *bool `json:"handle_chinese_chars"`
|
|
StripAccents *bool `json:"strip_accents"`
|
|
Lowercase *bool `json:"lowercase"`
|
|
} `json:"normalizer"`
|
|
PreTokenizer *struct {
|
|
Type string `json:"type"`
|
|
} `json:"pre_tokenizer"`
|
|
Model struct {
|
|
Type string `json:"type"`
|
|
UnkToken string `json:"unk_token"`
|
|
ContinuingSubwordPrefix string `json:"continuing_subword_prefix"`
|
|
MaxInputCharsPerWord *int `json:"max_input_chars_per_word"`
|
|
Vocab map[string]int `json:"vocab"`
|
|
} `json:"model"`
|
|
}
|
|
|
|
// loadWordPieceTokenizer parses a tokenizer.json from disk.
|
|
func loadWordPieceTokenizer(path string) (*wordPieceTokenizer, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read tokenizer: %w", err)
|
|
}
|
|
var tj tokenizerJSON
|
|
if err := json.Unmarshal(raw, &tj); err != nil {
|
|
return nil, fmt.Errorf("parse tokenizer: %w", err)
|
|
}
|
|
if tj.Model.Type != "WordPiece" {
|
|
return nil, fmt.Errorf("unsupported tokenizer model %q (want WordPiece)", tj.Model.Type)
|
|
}
|
|
if len(tj.Model.Vocab) == 0 {
|
|
return nil, fmt.Errorf("tokenizer vocab is empty")
|
|
}
|
|
t := &wordPieceTokenizer{
|
|
vocab: tj.Model.Vocab,
|
|
contPrefix: tj.Model.ContinuingSubwordPrefix,
|
|
maxWordChars: 100,
|
|
// BertNormalizer defaults per the reference implementation.
|
|
cleanText: true,
|
|
handleCJK: true,
|
|
}
|
|
if t.contPrefix == "" {
|
|
t.contPrefix = "##"
|
|
}
|
|
if tj.Model.MaxInputCharsPerWord != nil && *tj.Model.MaxInputCharsPerWord > 0 {
|
|
t.maxWordChars = *tj.Model.MaxInputCharsPerWord
|
|
}
|
|
unk := tj.Model.UnkToken
|
|
if unk == "" {
|
|
unk = "[UNK]"
|
|
}
|
|
id, ok := t.vocab[unk]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unk token %q not in vocab", unk)
|
|
}
|
|
t.unkID = id
|
|
if n := tj.Normalizer; n != nil {
|
|
if n.CleanText != nil {
|
|
t.cleanText = *n.CleanText
|
|
}
|
|
if n.HandleCJK != nil {
|
|
t.handleCJK = *n.HandleCJK
|
|
}
|
|
if n.Lowercase != nil {
|
|
t.lowercase = *n.Lowercase
|
|
}
|
|
// strip_accents:null means "follow lowercase" in the reference
|
|
// implementation; an explicit value wins.
|
|
if n.StripAccents != nil {
|
|
t.stripAccents = *n.StripAccents
|
|
} else {
|
|
t.stripAccents = t.lowercase
|
|
}
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// Encode returns the WordPiece token IDs for text. No special tokens
|
|
// are added (the model's post_processor is null).
|
|
func (t *wordPieceTokenizer) Encode(text string) []int {
|
|
var ids []int
|
|
for _, word := range t.preTokenize(t.normalize(text)) {
|
|
ids = t.wordPiece(word, ids)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// normalize applies BertNormalizer: control-char cleanup, CJK padding,
|
|
// accent stripping (NFD, drop Mn), lowercasing.
|
|
func (t *wordPieceTokenizer) normalize(s string) string {
|
|
var b strings.Builder
|
|
b.Grow(len(s) + 8)
|
|
for _, r := range s {
|
|
if t.cleanText {
|
|
if r == 0 || r == 0xFFFD || isBertControl(r) {
|
|
continue
|
|
}
|
|
if isBertWhitespace(r) {
|
|
b.WriteByte(' ')
|
|
continue
|
|
}
|
|
}
|
|
if t.handleCJK && isCJK(r) {
|
|
b.WriteByte(' ')
|
|
b.WriteRune(r)
|
|
b.WriteByte(' ')
|
|
continue
|
|
}
|
|
b.WriteRune(r)
|
|
}
|
|
out := b.String()
|
|
if t.stripAccents {
|
|
decomposed := norm.NFD.String(out)
|
|
var sb strings.Builder
|
|
sb.Grow(len(decomposed))
|
|
for _, r := range decomposed {
|
|
if unicode.Is(unicode.Mn, r) {
|
|
continue
|
|
}
|
|
sb.WriteRune(r)
|
|
}
|
|
out = sb.String()
|
|
}
|
|
if t.lowercase {
|
|
out = strings.ToLower(out)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// preTokenize applies BertPreTokenizer: split on whitespace, then
|
|
// isolate each punctuation rune as its own token.
|
|
func (t *wordPieceTokenizer) preTokenize(s string) []string {
|
|
var words []string
|
|
var cur strings.Builder
|
|
flush := func() {
|
|
if cur.Len() > 0 {
|
|
words = append(words, cur.String())
|
|
cur.Reset()
|
|
}
|
|
}
|
|
for _, r := range s {
|
|
switch {
|
|
case isBertWhitespace(r):
|
|
flush()
|
|
case isBertPunct(r):
|
|
flush()
|
|
words = append(words, string(r))
|
|
default:
|
|
cur.WriteRune(r)
|
|
}
|
|
}
|
|
flush()
|
|
return words
|
|
}
|
|
|
|
// wordPiece runs the greedy longest-match-first sub-word split for one
|
|
// word, appending IDs to dst.
|
|
func (t *wordPieceTokenizer) wordPiece(word string, dst []int) []int {
|
|
runes := []rune(word)
|
|
if len(runes) > t.maxWordChars {
|
|
return append(dst, t.unkID)
|
|
}
|
|
start := 0
|
|
var pieces []int
|
|
for start < len(runes) {
|
|
end := len(runes)
|
|
id := -1
|
|
for end > start {
|
|
sub := string(runes[start:end])
|
|
if start > 0 {
|
|
sub = t.contPrefix + sub
|
|
}
|
|
if v, ok := t.vocab[sub]; ok {
|
|
id = v
|
|
break
|
|
}
|
|
end--
|
|
}
|
|
if id < 0 {
|
|
// No piece matched: the whole word becomes UNK, per the
|
|
// reference implementation.
|
|
return append(dst, t.unkID)
|
|
}
|
|
pieces = append(pieces, id)
|
|
start = end
|
|
}
|
|
return append(dst, pieces...)
|
|
}
|
|
|
|
// isBertControl mirrors the reference _is_control: category Cc/Cf,
|
|
// except tab / newline / carriage-return which count as whitespace.
|
|
func isBertControl(r rune) bool {
|
|
if r == '\t' || r == '\n' || r == '\r' {
|
|
return false
|
|
}
|
|
return unicode.Is(unicode.Cc, r) || unicode.Is(unicode.Cf, r)
|
|
}
|
|
|
|
// isBertWhitespace mirrors the reference _is_whitespace.
|
|
func isBertWhitespace(r rune) bool {
|
|
switch r {
|
|
case ' ', '\t', '\n', '\r':
|
|
return true
|
|
}
|
|
return unicode.Is(unicode.Zs, r)
|
|
}
|
|
|
|
// isBertPunct mirrors the reference _is_punctuation: the four ASCII
|
|
// symbol ranges plus every Unicode P* category rune.
|
|
func isBertPunct(r rune) bool {
|
|
if (r >= 33 && r <= 47) || (r >= 58 && r <= 64) || (r >= 91 && r <= 96) || (r >= 123 && r <= 126) {
|
|
return true
|
|
}
|
|
return unicode.IsPunct(r)
|
|
}
|
|
|
|
// isCJK mirrors the reference _is_chinese_char CJK block test.
|
|
func isCJK(r rune) bool {
|
|
switch {
|
|
case r >= 0x4E00 && r <= 0x9FFF,
|
|
r >= 0x3400 && r <= 0x4DBF,
|
|
r >= 0x20000 && r <= 0x2A6DF,
|
|
r >= 0x2A700 && r <= 0x2B73F,
|
|
r >= 0x2B740 && r <= 0x2B81F,
|
|
r >= 0x2B820 && r <= 0x2CEAF,
|
|
r >= 0xF900 && r <= 0xFAFF,
|
|
r >= 0x2F800 && r <= 0x2FA1F:
|
|
return true
|
|
}
|
|
return false
|
|
}
|