| """ |
| Frox AI Morph 1.1 β Tokenizer |
| Fully local, no gated repos. |
| |
| Improvements over Morph 1.0: |
| - 64K vocab BPE (better multilingual, math, code) |
| - More special tokens (reasoning, citations, memory, search) |
| - DPO formatting helpers (chosen/rejected pairs) |
| - Chat template for system / user / assistant / tool roles |
| - Token budget estimation utility |
| """ |
| from __future__ import annotations |
| import os |
| import tempfile |
| from pathlib import Path |
| from typing import Dict, List, Optional, Sequence |
|
|
| |
|
|
| CHAT_SPECIAL_TOKENS = [ |
| "<|pad|>", |
| "<|begin_of_text|>", |
| "<|end_of_text|>", |
| "<|unk|>", |
| "<|eot_id|>", |
| "<|start_header_id|>", |
| "<|end_header_id|>", |
| "<|finetune_right_pad_id|>", |
| "<|step_id|>", |
| "<|python_tag|>", |
| ] |
|
|
| MORPH_SPECIAL_TOKENS = [ |
| |
| "<|image|>", |
| "<|video|>", |
| "<|audio|>", |
| |
| "<|gen_image|>", |
| "<|gen_video|>", |
| "<|gen_3d|>", |
| "<|/gen|>", |
| |
| "<|tool_call|>", |
| "<|/tool_call|>", |
| "<|tool_result|>", |
| "<|/tool_result|>", |
| |
| "<|think|>", |
| "<|/think|>", |
| |
| "<|cite|>", |
| "<|/cite|>", |
| |
| "<|memory|>", |
| "<|/memory|>", |
| |
| "<|search_result|>", |
| "<|/search_result|>", |
| |
| "<|code_output|>", |
| "<|/code_output|>", |
| |
| "<|fim_prefix|>", |
| "<|fim_middle|>", |
| "<|fim_suffix|>", |
| |
| "<|browser_result|>", |
| "<|/browser_result|>", |
| ] |
|
|
| ALL_SPECIAL_TOKENS = CHAT_SPECIAL_TOKENS + MORPH_SPECIAL_TOKENS |
|
|
| |
| PAD_TOKEN_ID = 0 |
| BOS_TOKEN_ID = 1 |
| EOS_TOKEN_ID = 2 |
| UNK_TOKEN_ID = 3 |
| IMAGE_TOKEN_ID = len(CHAT_SPECIAL_TOKENS) |
| VIDEO_TOKEN_ID = len(CHAT_SPECIAL_TOKENS) + 1 |
|
|
|
|
| |
|
|
| def build_morph_tokenizer( |
| tokenizer_path: Optional[str] = None, |
| corpus_files: Optional[Sequence[str]] = None, |
| save_path: Optional[str] = None, |
| vocab_size: int = 64000, |
| ): |
| """ |
| Build Frox Morph 1.1 tokenizer β fully local, zero auth. |
| |
| Priority: |
| 1. Load from saved tokenizer_path (fastest) |
| 2. Train BPE from provided corpus_files |
| 3. Auto-download free corpus (WikiText-103, ~180MB) and train |
| Falls back to synthetic corpus if download fails. |
| |
| Args: |
| tokenizer_path: Previously saved tokenizer dir (HF format). |
| corpus_files: .txt / .jsonl files for BPE training. |
| save_path: Where to save after building. |
| vocab_size: BPE vocab size (64K default for 1.1). |
| """ |
| print(f"\nπ€ Building Morph 1.1 Tokenizer (vocab_size={vocab_size:,})") |
|
|
| |
| if tokenizer_path and Path(tokenizer_path).exists(): |
| from transformers import AutoTokenizer |
| tok = AutoTokenizer.from_pretrained(tokenizer_path, use_fast=True) |
| _ensure_special_tokens(tok) |
| print(f" β Loaded from: {tokenizer_path} ({len(tok):,} tokens)") |
| if save_path: |
| _save(tok, save_path) |
| return tok |
|
|
| |
| if corpus_files: |
| valid = [p for p in corpus_files if Path(p).exists()] |
| if not valid: |
| raise FileNotFoundError(f"No corpus files found: {corpus_files}") |
| print(f" Training BPE from {len(valid)} file(s)...") |
| tok_model = _train_bpe(valid, vocab_size) |
| tok = _wrap_tokenizer(tok_model) |
| print(f" β Trained ({len(tok):,} tokens)") |
| if save_path: |
| _save(tok, save_path) |
| return tok |
|
|
| |
| print(" No corpus provided β downloading WikiText-103...") |
| corpus_path = _download_corpus() |
| tok_model = _train_bpe([corpus_path], vocab_size) |
| tok = _wrap_tokenizer(tok_model) |
| print(f" β Built ({len(tok):,} tokens)") |
| if save_path: |
| _save(tok, save_path) |
| return tok |
|
|
|
|
| |
|
|
| def _download_corpus() -> str: |
| """Download WikiText-103 train split (public domain, ~180MB).""" |
| import urllib.request |
|
|
| save_dir = Path(tempfile.gettempdir()) / "morph11_corpus" |
| save_dir.mkdir(exist_ok=True) |
| out = save_dir / "wikitext103.txt" |
|
|
| if out.exists() and out.stat().st_size > 50_000_000: |
| print(f" Using cached: {out}") |
| return str(out) |
|
|
| |
| url = "https://huggingface.co/datasets/Salesforce/wikitext/resolve/main/wikitext-103-raw-v1/train-00000-of-00002.parquet" |
| alt = "https://raw.githubusercontent.com/pytorch/examples/main/word_language_model/data/wikitext-2/train.txt" |
|
|
| try: |
| print(f" Downloading WikiText-103...") |
| |
| from datasets import load_dataset |
| ds = load_dataset("Salesforce/wikitext", "wikitext-103-raw-v1", |
| split="train", trust_remote_code=False) |
| with open(out, "w", encoding="utf-8") as f: |
| for ex in ds: |
| t = ex.get("text", "").strip() |
| if t: |
| f.write(t + "\n") |
| size_mb = out.stat().st_size / 1e6 |
| print(f" β Downloaded {size_mb:.1f} MB") |
| return str(out) |
| except Exception as e: |
| print(f" datasets lib failed ({e}), falling back to WikiText-2...") |
|
|
| try: |
| urllib.request.urlretrieve(alt, str(out)) |
| return str(out) |
| except Exception as e: |
| print(f" Download failed ({e}) β using synthetic corpus") |
| return _make_synthetic_corpus(save_dir) |
|
|
|
|
| def _make_synthetic_corpus(save_dir: Path) -> str: |
| """Minimal synthetic corpus so training never crashes.""" |
| path = save_dir / "synthetic.txt" |
| lines = [ |
| "The quick brown fox jumps over the lazy dog.", |
| "Artificial intelligence is reshaping every industry.", |
| "Language models learn patterns from large text datasets.", |
| "Neural networks compute features through multiple layers.", |
| "Frox AI Morph 1.1 is a powerful multimodal assistant.", |
| "You can ask me to generate images, videos, and 3D objects.", |
| "Reasoning models can solve complex mathematical problems.", |
| "The transformer architecture uses self-attention mechanisms.", |
| "Tokenization converts raw text into subword tokens.", |
| "Byte pair encoding merges frequent character pairs iteratively.", |
| "Training requires compute, data, and careful hyperparameter tuning.", |
| "Supervised fine-tuning teaches models to follow instructions.", |
| "Direct preference optimization improves alignment with human values.", |
| "Retrieval augmented generation grounds answers in real documents.", |
| ] * 10000 |
| path.write_text("\n".join(lines), encoding="utf-8") |
| print(f" Synthetic corpus: {len(lines):,} sentences") |
| return str(path) |
|
|
|
|
| |
|
|
| def _train_bpe(corpus_paths: List[str], vocab_size: int): |
| """Train byte-level BPE from corpus files.""" |
| from tokenizers import Tokenizer |
| from tokenizers.models import BPE |
| from tokenizers.pre_tokenizers import ByteLevel |
| from tokenizers.decoders import ByteLevel as ByteLevelDecoder |
| from tokenizers.trainers import BpeTrainer |
| from tokenizers.normalizers import NFKC |
|
|
| tok = Tokenizer(BPE(unk_token="<|unk|>")) |
| tok.normalizer = NFKC() |
| tok.pre_tokenizer = ByteLevel(add_prefix_space=False) |
| tok.decoder = ByteLevelDecoder() |
|
|
| trainer = BpeTrainer( |
| vocab_size=vocab_size, |
| special_tokens=ALL_SPECIAL_TOKENS, |
| initial_alphabet=ByteLevel.alphabet(), |
| show_progress=True, |
| min_frequency=2, |
| ) |
| tok.train(corpus_paths, trainer) |
| return tok |
|
|
|
|
| def _wrap_tokenizer(tok_model): |
| """Wrap tokenizers.Tokenizer into HuggingFace PreTrainedTokenizerFast.""" |
| from transformers import PreTrainedTokenizerFast |
| tok = PreTrainedTokenizerFast( |
| tokenizer_object=tok_model, |
| bos_token="<|begin_of_text|>", |
| eos_token="<|end_of_text|>", |
| unk_token="<|unk|>", |
| pad_token="<|pad|>", |
| additional_special_tokens=ALL_SPECIAL_TOKENS, |
| ) |
| tok.chat_template = MORPH_CHAT_TEMPLATE |
| return tok |
|
|
|
|
| def _ensure_special_tokens(tok): |
| """Ensure all Morph 1.1 special tokens exist in the loaded tokenizer.""" |
| missing = [t for t in ALL_SPECIAL_TOKENS if t not in tok.get_vocab()] |
| if missing: |
| tok.add_special_tokens({"additional_special_tokens": missing}) |
| print(f" Added {len(missing)} missing special tokens") |
| for attr, token in [("pad_token","<|pad|>"), ("bos_token","<|begin_of_text|>"), |
| ("eos_token","<|end_of_text|>")]: |
| if getattr(tok, attr) is None: |
| tok.add_special_tokens({attr: token}) |
|
|
|
|
| def _save(tok, save_path: str): |
| Path(save_path).mkdir(parents=True, exist_ok=True) |
| tok.save_pretrained(save_path) |
| print(f" β Saved to: {save_path}") |
|
|
|
|
| |
|
|
| MORPH_CHAT_TEMPLATE = ( |
| "{%- set ns = namespace(system='') -%}" |
| "{%- if messages[0]['role'] == 'system' -%}" |
| "{%- set ns.system = messages[0]['content'] -%}" |
| "{%- set messages = messages[1:] -%}" |
| "{%- endif -%}" |
| "<|begin_of_text|>" |
| "{%- if ns.system -%}" |
| "<|start_header_id|>system<|end_header_id|>\n" |
| "{{ ns.system }}<|eot_id|>" |
| "{%- endif -%}" |
| "{%- for message in messages -%}" |
| "<|start_header_id|>{{ message['role'] }}<|end_header_id|>\n" |
| "{{ message['content'] }}<|eot_id|>" |
| "{%- endfor -%}" |
| "{%- if add_generation_prompt -%}" |
| "<|start_header_id|>assistant<|end_header_id|>\n" |
| "{%- endif -%}" |
| ) |
|
|
|
|
| def apply_chat_template( |
| messages: List[Dict[str, str]], |
| tokenizer, |
| add_generation_prompt: bool = True, |
| system_prompt: Optional[str] = None, |
| ) -> str: |
| """Apply Morph 1.1 chat template to a message list.""" |
| if system_prompt and (not messages or messages[0]["role"] != "system"): |
| messages = [{"role": "system", "content": system_prompt}] + list(messages) |
|
|
| try: |
| return tokenizer.apply_chat_template( |
| messages, |
| tokenize=False, |
| add_generation_prompt=add_generation_prompt, |
| ) |
| except Exception: |
| |
| parts = ["<|begin_of_text|>"] |
| for msg in messages: |
| parts.append( |
| f"<|start_header_id|>{msg['role']}<|end_header_id|>\n" |
| f"{msg['content']}<|eot_id|>" |
| ) |
| if add_generation_prompt: |
| parts.append("<|start_header_id|>assistant<|end_header_id|>\n") |
| return "".join(parts) |
|
|
|
|
| def format_tool_call(tool_name: str, args: dict) -> str: |
| """Format a tool call in Morph 1.1 format.""" |
| import json |
| return f"<|tool_call|>{json.dumps({'name': tool_name, 'args': args})}<|/tool_call|>" |
|
|
|
|
| def format_tool_result(tool_name: str, result) -> str: |
| """Format a tool result for injection back to the model.""" |
| import json |
| return f"<|tool_result|>{json.dumps({'name': tool_name, 'result': result})}<|/tool_result|>" |
|
|
|
|
| def format_thinking(thinking: str) -> str: |
| """Format chain-of-thought reasoning block.""" |
| return f"<|think|>{thinking}<|/think|>" |
|
|
|
|
| def format_fim(prefix: str, suffix: str) -> str: |
| """ |
| Format a fill-in-middle prompt (PSM order: prefix, suffix, then the |
| model generates the middle). Used by Morph Code for code infilling β |
| e.g. completing a function body given the code before and after it. |
| """ |
| return f"<|fim_prefix|>{prefix}<|fim_suffix|>{suffix}<|fim_middle|>" |
|
|
|
|
| |
|
|
| def format_dpo_pair( |
| messages: List[Dict[str, str]], |
| chosen: str, |
| rejected: str, |
| tokenizer, |
| system_prompt: Optional[str] = None, |
| ) -> Dict[str, str]: |
| """ |
| Format a preference pair for DPO training. |
| |
| Returns: |
| { |
| "prompt": "<prompt without final assistant turn>", |
| "chosen": "<prompt + chosen response>", |
| "rejected": "<prompt + rejected response>", |
| } |
| """ |
| prompt = apply_chat_template( |
| messages, tokenizer, add_generation_prompt=True, |
| system_prompt=system_prompt, |
| ) |
| assistant_suffix = "<|start_header_id|>assistant<|end_header_id|>\n" |
|
|
| return { |
| "prompt": prompt, |
| "chosen": prompt + chosen + "<|eot_id|>", |
| "rejected": prompt + rejected + "<|eot_id|>", |
| } |
|
|
|
|
| |
|
|
| def count_tokens(text: str, tokenizer) -> int: |
| """Fast token count without full encode.""" |
| return len(tokenizer.encode(text, add_special_tokens=False)) |
|
|
|
|
| def estimate_cost_tokens( |
| messages: List[Dict[str, str]], |
| max_new_tokens: int, |
| tokenizer, |
| ) -> Dict[str, int]: |
| """Estimate total tokens for a request.""" |
| prompt_str = apply_chat_template(messages, tokenizer, add_generation_prompt=True) |
| prompt_tokens = count_tokens(prompt_str, tokenizer) |
| return { |
| "prompt_tokens": prompt_tokens, |
| "max_completion": max_new_tokens, |
| "total_max": prompt_tokens + max_new_tokens, |
| } |
|
|