chore: import upstream snapshot with attribution
build / build (macos-latest) (push) Has been cancelled
build / build (ubuntu-latest) (push) Has been cancelled
build / build (windows-latest) (push) Has been cancelled
minimal / deploy (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:38:00 +08:00
commit 3a7c47b2a6
623 changed files with 133790 additions and 0 deletions
@@ -0,0 +1,29 @@
"""
AudioMixer module tests
"""
import unittest
import numpy as np
from txtai.pipeline import AudioMixer
class TestAudioStream(unittest.TestCase):
"""
AudioStream tests.
"""
def testAudioStream(self):
"""
Test mixing audio streams
"""
audio1 = np.random.rand(2, 5000), 100
audio2 = np.random.rand(2, 5000), 100
mixer = AudioMixer()
audio, rate = mixer((audio1, audio2))
self.assertEqual(audio.shape, (2, 5000))
self.assertEqual(rate, 100)
@@ -0,0 +1,37 @@
"""
AudioStream module tests
"""
import unittest
from unittest.mock import patch
import soundfile as sf
from txtai.pipeline import AudioStream
# pylint: disable=C0411
from utils import Utils
class TestAudioStream(unittest.TestCase):
"""
AudioStream tests.
"""
@patch("sounddevice.play")
def testAudioStream(self, play):
"""
Test playing audio
"""
play.return_value = True
# Read audio data
audio, rate = sf.read(Utils.PATH + "/Make_huge_profits.wav")
stream = AudioStream()
self.assertIsNotNone(stream([(audio, rate), AudioStream.COMPLETE]))
# Wait for completion
stream.wait()
@@ -0,0 +1,81 @@
"""
Microphone module tests
"""
import unittest
from unittest.mock import patch
import numpy as np
import soundfile as sf
from txtai.pipeline import Microphone
# pylint: disable=C0411
from utils import Utils
class TestMicrophone(unittest.TestCase):
"""
Microphone tests.
"""
# pylint: disable=C0115,C0116
@patch("sounddevice.RawInputStream")
def testMicrophone(self, inputstream):
"""
Test listening to microphone
"""
class RawInputStream:
def __init__(self, **kwargs):
self.args = kwargs
# Read audio data
self.index, self.passes = 0, 0
audio, self.samplerate = sf.read(Utils.PATH + "/Make_huge_profits.wav")
# Convert data to PCM
self.audio = self.int16(audio)
# Start with random data to test that speech is not detected
self.data = np.concatenate((self.audio * 50, np.zeros(shape=self.audio.shape, dtype=np.int16)))
def start(self):
pass
def stop(self):
pass
def read(self, size):
# Get chunk
chunk = self.data[self.index : self.index + size]
self.index += size
# Initial pass is random data, 2nd pass is speech data
if self.index > len(self.data):
if not self.passes:
self.index, self.passes = 0, self.passes + 1
self.data = self.audio
elif self.index >= len(self.audio) * 10:
# Break out of loop if speech continues to not be detected
raise IOError("Data exhausted")
return chunk, False
def int16(self, data):
i = np.iinfo(np.int16)
absmax = 2 ** (i.bits - 1)
offset = i.min + absmax
return (data * absmax + offset).clip(i.min, i.max).astype(np.int16)
# Mock input stream
inputstream.side_effect = RawInputStream
# Create microphone pipeline and read data
pipeline = Microphone()
data, rate = pipeline()
# Validate sample rate and length of data
self.assertEqual(len(data), 91220)
self.assertEqual(rate, 16000)
@@ -0,0 +1,26 @@
"""
TextToAudio module tests
"""
import unittest
from txtai.pipeline import TextToAudio
class TestTextToAudio(unittest.TestCase):
"""
TextToAudio tests.
"""
def testTextToAudio(self):
"""
Test generating audio for text
"""
tta = TextToAudio("hf-internal-testing/tiny-random-MusicgenForConditionalGeneration")
# Check that data is generated
audio, rate = tta("This is a test")
self.assertGreater(len(audio), 0)
self.assertEqual(rate, 24000)
@@ -0,0 +1,82 @@
"""
TextToSpeech module tests
"""
import unittest
from unittest.mock import patch
from txtai.pipeline import TextToSpeech
class TestTextToSpeech(unittest.TestCase):
"""
TextToSpeech tests.
"""
def testESPnet(self):
"""
Test generating speech for text with an ESPnet model
"""
tts = TextToSpeech()
# Check that data is generated
speech, rate = tts("This is a test")
self.assertGreater(len(speech), 0)
self.assertEqual(rate, 22050)
def testKokoro(self):
"""
Test generating speech for text with a Kokoro model
"""
tts = TextToSpeech("neuml/kokoro-int8-onnx", maxtokens=2)
# Check that data is generated
speech, rate = tts("This is a test")
self.assertGreater(len(speech), 0)
self.assertEqual(rate, 22050)
@patch("onnxruntime.get_available_providers")
@patch("torch.cuda.is_available")
def testProviders(self, cuda, providers):
"""
Test that GPU provider is detected
"""
# Test CUDA and onnxruntime-gpu installed
cuda.return_value = True
providers.return_value = ["CUDAExecutionProvider", "CPUExecutionProvider"]
tts = TextToSpeech()
self.assertEqual(tts.providers()[0][0], "CUDAExecutionProvider")
def testSpeechT5(self):
"""
Test generating speech for text with a SpeechT5 model
"""
tts = TextToSpeech("neuml/txtai-speecht5-onnx")
# Check that data is generated
speech, rate = tts("This is a test")
self.assertGreater(len(speech), 0)
self.assertEqual(rate, 22050)
def testStreaming(self):
"""
Test streaming speech generation
"""
tts = TextToSpeech()
# Check that data is generated
speech, rate = list(tts("This is a test. And another".split(), stream=True))[0]
# Check that data is generated
self.assertGreater(len(speech), 0)
self.assertEqual(rate, 22050)
@@ -0,0 +1,104 @@
"""
Transcription module tests
"""
import unittest
import numpy as np
import soundfile as sf
from scipy import signal
from txtai.pipeline import Transcription
# pylint: disable=C0411
from utils import Utils
class TestTranscription(unittest.TestCase):
"""
Transcription tests.
"""
def testArray(self):
"""
Test audio data to text transcription
"""
transcribe = Transcription()
# Read audio data
raw, samplerate = sf.read(Utils.PATH + "/Make_huge_profits.wav")
self.assertEqual(transcribe((raw, samplerate)), "Make huge profits without working make up to one hundred thousand dollars a day")
self.assertEqual(transcribe(raw, samplerate), "Make huge profits without working make up to one hundred thousand dollars a day")
def testChunks(self):
"""
Test splitting transcription into chunks
"""
transcribe = Transcription()
result = transcribe(Utils.PATH + "/Make_huge_profits.wav", join=False)[0]
self.assertIsInstance(result["raw"], np.ndarray)
self.assertIsNotNone(result["rate"])
self.assertEqual(result["text"], "Make huge profits without working make up to one hundred thousand dollars a day")
def testFile(self):
"""
Test audio file to text transcription
"""
transcribe = Transcription()
self.assertEqual(
transcribe(Utils.PATH + "/Make_huge_profits.wav"), "Make huge profits without working make up to one hundred thousand dollars a day"
)
def testGenerateArguments(self):
"""
Test transcription with generation keyword arguments
"""
transcribe = Transcription()
# Read audio data
raw, samplerate = sf.read(Utils.PATH + "/Make_huge_profits.wav")
self.assertEqual(
transcribe(raw, samplerate, language="English", task="transcribe"),
"Make huge profits without working make up to one hundred thousand dollars a day",
)
def testResample(self):
"""
Test resampled audio file to text transcription
"""
transcribe = Transcription()
# Read audio data
raw, samplerate = sf.read(Utils.PATH + "/Make_huge_profits.wav")
# Resample for testing
samples = round(len(raw) * float(22050) / samplerate)
raw, samplerate = signal.resample(raw, samples), 22050
self.assertEqual(transcribe(raw, samplerate), "Make huge profits without working make up to one hundred thousand dollars a day")
def testStereo(self):
"""
Test audio file in stereo to text transcription
"""
transcribe = Transcription()
# Read audio data
raw, samplerate = sf.read(Utils.PATH + "/Make_huge_profits.wav")
# Convert mono to stereo
raw = np.column_stack((raw, raw))
self.assertEqual(transcribe(raw, samplerate), "Make huge profits without working make up to one hundred thousand dollars a day")
View File
+24
View File
@@ -0,0 +1,24 @@
"""
FileToHTML module tests
"""
import os
import unittest
from unittest.mock import patch
from txtai.pipeline.data.filetohtml import Tika
class TestFileToHTML(unittest.TestCase):
"""
FileToHTML tests.
"""
@patch.dict(os.environ, {"TIKA_JAVA": "1112444abc"})
def testTika(self):
"""
Test the Tika.available returns False when Java is not available
"""
self.assertFalse(Tika.available())
+123
View File
@@ -0,0 +1,123 @@
"""
Tabular module tests
"""
import unittest
from txtai.pipeline import Tabular
# pylint: disable=C0411
from utils import Utils
class TestTabular(unittest.TestCase):
"""
Tabular tests.
"""
@classmethod
def setUpClass(cls):
"""
Create single tabular instance
"""
cls.tabular = Tabular("id", ["text"])
def testContent(self):
"""
Test parsing additional content
"""
tabular = Tabular("id", ["text"], True)
row = {"id": 0, "text": "This is a test", "flag": 1}
# When content is enabled, both (uid, text, tags) and (uid, data, tags) rows are generated
# given that data doesn't necessarily include the text to index
rows = tabular([row])
uid, data, _ = rows[1]
# Data should contain the entire input row
self.assertEqual(uid, 0)
self.assertEqual(data, row)
# Only select flag field
tabular.content = ["flag"]
rows = tabular([row])
uid, data, _ = rows[1]
# Data should only contain a single field, flag
self.assertEqual(uid, 0)
self.assertTrue(list(data.keys()) == ["flag"])
self.assertEqual(data["flag"], 1)
def testCSV(self):
"""
Test parsing a CSV file
"""
rows = self.tabular([Utils.PATH + "/tabular.csv"])
uid, text, _ = rows[0][0]
self.assertEqual(uid, 0)
self.assertEqual(text, "The first sentence")
def testDict(self):
"""
Test parsing a dict
"""
rows = self.tabular([{"id": 0, "text": "This is a test"}])
uid, text, _ = rows[0]
self.assertEqual(uid, 0)
self.assertEqual(text, "This is a test")
def testInvalid(self):
"""
Test invalid file paths
"""
with self.assertRaises(ValueError):
self.tabular([Utils.PATH + "/article.pdf"])
with self.assertRaises(ValueError):
self.tabular(["https://invalid.path"])
def testList(self):
"""
Test parsing a list
"""
rows = self.tabular([[{"id": 0, "text": "This is a test"}]])
uid, text, _ = rows[0][0]
self.assertEqual(uid, 0)
self.assertEqual(text, "This is a test")
def testMissingColumns(self):
"""
Test rows with uneven or missing columns
"""
tabular = Tabular("id", ["text"], True)
rows = tabular([{"id": 0, "text": "This is a test", "metadata": "meta"}, {"id": 1, "text": "This is a test"}])
# When content is enabled both (id, text, tag) and (id, data, tag) tuples are generated given that
# data doesn't necessarily include the text to index
_, data, _ = rows[3]
self.assertIsNone(data["metadata"])
def testNoColumns(self):
"""
Test creating text without specifying columns
"""
tabular = Tabular("id")
rows = tabular([{"id": 0, "text": "This is a test", "summary": "Describes text in more detail"}])
uid, text, _ = rows[0]
self.assertEqual(uid, 0)
self.assertEqual(text, "This is a test. Describes text in more detail")
+253
View File
@@ -0,0 +1,253 @@
"""
Textractor module tests
"""
import platform
import unittest
from txtai.pipeline import Textractor
# pylint: disable=C0411
from utils import Utils
class TestTextractor(unittest.TestCase):
"""
Textractor tests.
"""
def testClean(self):
"""
Test text cleaning method
"""
# Default text cleaning
textractor = Textractor()
self.assertEqual(textractor(" a b c "), "a b c")
# Require text to be minlength
textractor = Textractor(minlength=10)
self.assertEqual(textractor(" a b c "), None)
# Disable text cleaning
textractor = Textractor(cleantext=False, minlength=10)
self.assertEqual(textractor(" a b c "), " a b c ")
def testChonkie(self):
"""
Test a chonkie chunker
"""
# Test chonkie chunking
textractor = Textractor(chunker="sentence", chunk_size=5, chunk_overlap=0)
self.assertEqual(textractor("This is a test. And another test."), ["This is a test.", "And another test."])
# Test bad chunker throws an exception
with self.assertRaises(AttributeError):
textractor = Textractor(chunker="badchunker")
def testDefault(self):
"""
Test default text extraction
"""
# Text input
textractor = Textractor(backend=None)
text = textractor(Utils.PATH + "/tabular.csv")
self.assertEqual(len(text), 125)
# Markdown input
textractor = Textractor(sections=True)
sections = textractor("# Heading 1\nText1\n\n# Heading 2\nText2\n")
# Check number of sections is as expected
self.assertEqual(len(sections), 2)
@unittest.skipIf(platform.system() == "Darwin", "Docling skipped on macOS to avoid MPS issues")
def testDocling(self):
"""
Test docling backend
"""
textractor = Textractor(backend="docling")
# Extract text and check for Markdown formatting
text = textractor(Utils.PATH + "/article.pdf")
self.assertTrue("## Introducing txtai" in text)
def testLines(self):
"""
Test extraction to lines
"""
textractor = Textractor(lines=True)
# Extract text as lines
lines = textractor(Utils.PATH + "/article.pdf")
# Check number of lines is as expected
self.assertEqual(len(lines), 35)
def testLiteParse(self):
"""
Test liteparse backend
"""
textractor = Textractor(backend="liteparse")
# Extract text and check for Markdown formatting
text = textractor(Utils.PATH + "/article.pdf")
self.assertTrue("# Introducing txtai" in text)
def testHTML(self):
"""
Test HTML to Markdown
"""
# Headings
self.assertMarkdown("<h1>This is a test</h1>", "# This is a test")
self.assertMarkdown("<h6>This is a test</h6>", "###### This is a test")
# Blockquotes
self.assertMarkdown("<blockquote>This is a test</blockquote>", "> This is a test")
# Lists
self.assertMarkdown("<ul><li>Test1</li><li>Test2</li></ul>", "- Test1\n- Test2")
self.assertMarkdown("<ol><li>Test1</li><li>Test2</li></ol>", "1. Test1\n2. Test2")
# Code
self.assertMarkdown("<code>This is a test</code>", "```\nThis is a test\n```")
self.assertMarkdown("<pre>This is a test</pre>", "```\nThis is a test\n```")
# Tables
self.assertMarkdown(
"<table><tr><th>Header1</th><th>Header2</th></tr><tr><td>Test1</td><td>Test2</td></tr></table>",
"|Header1|Header2|\n|---|---|\n|Test1|Test2|",
)
# Ignore list
self.assertMarkdown("<aside>This is a test</aside>", "")
# Text formatting
self.assertMarkdown("<p>This is a test</p>", "This is a test")
self.assertMarkdown("<p>This is a <b>test</b</p>", "This is a **test**")
self.assertMarkdown("<p>This is a <strong>test</strong></p>", "This is a **test**")
self.assertMarkdown("<p>This is a <i>test</i></p>", "This is a *test*")
self.assertMarkdown("<p>This is a <em>test</em></p>", "This is a *test*")
self.assertMarkdown("<p>This is a <a href='link'>test</a>", "This is a [test](link)")
# Collapse to outer tag
self.assertMarkdown("<p>This is a <strong><em>test</em></strong></p>", "This is a **test**")
self.assertMarkdown("<p>This is a <em><strong>test</strong></em></p>", "This is a *test*")
def testParagraphs(self):
"""
Test extraction to paragraphs
"""
textractor = Textractor(paragraphs=True)
# Extract text as paragraphs
paragraphs = textractor(Utils.PATH + "/article.pdf")
# Check number of paragraphs is as expected
self.assertEqual(len(paragraphs), 11)
def testSections(self):
"""
Test extraction to sections
"""
textractor = Textractor(sections=True)
# Extract as sections
sections = textractor(Utils.PATH + "/document.pdf")
# Check number of sections is as expected
self.assertEqual(len(sections), 3)
def testSentences(self):
"""
Test extraction to sentences
"""
textractor = Textractor(sentences=True)
# Extract text as sentences
sentences = textractor(Utils.PATH + "/article.pdf")
# Check number of sentences is as expected
self.assertEqual(len(sentences), 17)
def testSingle(self):
"""
Test a single extraction with no tokenization of the results
"""
textractor = Textractor()
# Extract text as a single block
text = textractor(Utils.PATH + "/article.pdf")
# Check length of text is as expected
self.assertEqual(len(text), 2471)
def testTable(self):
"""
Test table extraction
"""
textractor = Textractor()
# Extract text as a single block
for name in ["document.docx", "spreadsheet.xlsx"]:
text = textractor(f"{Utils.PATH}/{name}")
# Check for table header
self.assertTrue("|---|" in text)
def testTikaFlag(self):
"""
Test legacy tika flag
"""
textractor = Textractor(tika=True)
self.assertIsNotNone(textractor.html)
textractor = Textractor(tika=False)
self.assertIsNone(textractor.html)
def testTuples(self):
"""
Test output tuples
"""
# Default text cleaning
textractor = Textractor(tuples=True)
path, text = textractor(Utils.PATH + "/article.pdf")
self.assertEqual(path, Utils.PATH + "/article.pdf")
self.assertEqual(len(text), 2471)
def testURL(self):
"""
Test parsing a remote URL
"""
# Test parsing URLs for each backend
for backend in ["docling", "liteparse", "tika"]:
textractor = Textractor(backend=backend)
text = textractor("https://github.com/neuml/txtai")
self.assertTrue("txtai is an all-in-one AI framework" in text)
def assertMarkdown(self, html, expected):
"""
Helper method to assert generated markdown is as expected.
Args:
html: input html snippet
expected: expected markdown text
"""
textractor = Textractor()
self.assertEqual(textractor(f"<html><body>{html}</body></html>"), expected)
+68
View File
@@ -0,0 +1,68 @@
"""
Tokenizer module tests
"""
import unittest
from txtai.pipeline import Tokenizer
class TestTokenizer(unittest.TestCase):
"""
Tokenizer tests.
"""
def testAlphanumTokenize(self):
"""
Test alphanumeric tokenization
"""
# Alphanumeric tokenization through backwards compatible static method
self.assertEqual(Tokenizer.tokenize("Y this is a test!"), ["test"])
self.assertEqual(Tokenizer.tokenize("abc123 ABC 123"), ["abc123", "abc"])
def testEmptyTokenize(self):
"""
Test handling empty and None inputs
"""
# Test that parser can handle empty or None strings
self.assertEqual(Tokenizer.tokenize(""), [])
self.assertEqual(Tokenizer.tokenize(None), None)
def testStandardTokenize(self):
"""
Test standard tokenization
"""
# Default standard tokenizer parameters
tokenizer = Tokenizer()
# Define token tests
tests = [
("Y this is a test!", ["y", "this", "is", "a", "test"]),
("abc123 ABC 123", ["abc123", "abc", "123"]),
("Testing hy-phenated words", ["testing", "hy", "phenated", "words"]),
("111-111-1111", ["111", "111", "1111"]),
("Test.1234", ["test", "1234"]),
]
# Run through tests
for test, result in tests:
# Unicode Text Segmentation per Unicode Annex #29
self.assertEqual(tokenizer(test), result)
def testNgramTokenize(self):
"""
Test ngram tokenization
"""
# Standard ngram tokenization
tokenizer = Tokenizer(lowercase=True, ngrams=3)
result = tokenizer("NGRAM TEST")
self.assertIn("ngr", result)
# Case sensitive ngram tokenization
tokenizer = Tokenizer(lowercase=False, ngrams=3)
result = tokenizer("NGRAM TEST")
self.assertIn("NGR", result)
+115
View File
@@ -0,0 +1,115 @@
"""
URLRetrieve module tests
"""
import contextlib
import unittest
from http.server import HTTPServer, BaseHTTPRequestHandler
from threading import Thread
from urllib.request import build_opener
from txtai.pipeline import URLRetrieve
from txtai.pipeline.data.urlretrieve import SafeRedirectHandler
class RequestHandler(BaseHTTPRequestHandler):
"""
Test HTTP handler.
"""
def do_GET(self):
"""
GET request handler.
"""
if self.path == "/valid":
redirect = "https://github.com/neuml/txtai"
elif self.path == "/invalid":
redirect = "http://127.0.0.1"
else:
redirect = None
if redirect:
self.send_response(301)
self.send_header("Location", redirect)
self.end_headers()
else:
response = "test".encode("utf-8")
self.send_response(200)
self.send_header("content-type", "text/plain")
self.send_header("content-length", len(response))
self.end_headers()
self.wfile.write(response)
self.wfile.flush()
class TestURLRetrieve(unittest.TestCase):
"""
URLRetrieve tests.
"""
@classmethod
def setUpClass(cls):
"""
Create mock http server
"""
cls.httpd = HTTPServer(("127.0.0.1", 8006), RequestHandler)
server = Thread(target=cls.httpd.serve_forever, daemon=True)
server.start()
@classmethod
def tearDownClass(cls):
"""
Shutdown mock http server.
"""
cls.httpd.shutdown()
def testRedirect(self):
"""
Test redirects
"""
urlretrieve = URLRetrieve(safeopen=True)
# Test redirect handler
opener = build_opener(SafeRedirectHandler(urlretrieve))
# Test valid direct
with contextlib.closing(opener.open("http://127.0.0.1:8006/valid")) as connection:
self.assertTrue("txtai is an all-in-one AI framework" in str(connection.read()))
# Test invalid redirect
with self.assertRaises(IOError):
contextlib.closing(opener.open("http://127.0.0.1:8006/invalid"))
def testRetrieve(self):
"""
Test retrieval
"""
urlretrieve = URLRetrieve()
data = urlretrieve("http://127.0.0.1:8006/data")
self.assertEqual(data, b"test")
def testSafeopen(self):
"""
Test safeopen checks
"""
urlretrieve = URLRetrieve(safeopen=True)
# Verify that local ip addresses fail
with self.assertRaises(IOError):
urlretrieve("http://127.0.0.1")
with self.assertRaises(IOError):
urlretrieve("https://127.0.0.1")
with self.assertRaises(IOError):
urlretrieve("https://[::1]")
@@ -0,0 +1,36 @@
"""
Caption module tests
"""
import unittest
from PIL import Image
from transformers import AutoModelForImageTextToText, AutoImageProcessor, AutoTokenizer
from txtai.pipeline import Caption
# pylint: disable=C0411
from utils import Utils
class TestCaption(unittest.TestCase):
"""
Caption tests.
"""
def testCaption(self):
"""
Test captions
"""
caption = Caption()
self.assertEqual(caption(Image.open(Utils.PATH + "/books.jpg")), "a book shelf filled with books and a stack of books")
# Load passing models directly
path = "ydshieh/vit-gpt2-coco-en"
model = AutoModelForImageTextToText.from_pretrained(path)
tokenizer = AutoTokenizer.from_pretrained(path)
processor = AutoImageProcessor.from_pretrained(path)
caption = Caption((model, tokenizer, processor))
self.assertEqual(caption(Image.open(Utils.PATH + "/books.jpg")), "a book shelf filled with books and a stack of books")
@@ -0,0 +1,74 @@
"""
ImageHash module tests
"""
import unittest
from PIL import Image
from txtai.pipeline import ImageHash
# pylint: disable=C0411
from utils import Utils
class TestImageHash(unittest.TestCase):
"""
ImageHash tests.
"""
@classmethod
def setUpClass(cls):
"""
Caches an image to hash
"""
cls.image = Image.open(Utils.PATH + "/books.jpg")
def testArray(self):
"""
Test numpy return type
"""
ihash = ImageHash(strings=False)
self.assertEqual(ihash(self.image).shape, (64,))
def testAverage(self):
"""
Test average hash
"""
ihash = ImageHash("average")
self.assertIn(ihash(self.image), ["0859dd04bfbfbf00", "0859dd04ffbfbf00"])
def testColor(self):
"""
Test color hash
"""
ihash = ImageHash("color")
self.assertIn(ihash(self.image), ["1ffffe02000e000c0e0000070000", "1ff8fe03000e00070e0000070000"])
def testDifference(self):
"""
Test difference hash
"""
ihash = ImageHash("difference")
self.assertEqual(ihash(self.image), "d291996d6969686a")
def testPerceptual(self):
"""
Test perceptual hash
"""
ihash = ImageHash("perceptual")
self.assertEqual(ihash(self.image), "8be8418577b331b9")
def testWavelet(self):
"""
Test wavelet hash
"""
ihash = ImageHash("wavelet")
self.assertEqual(ihash(Utils.PATH + "/books.jpg"), "68015d85bfbf3f00")
@@ -0,0 +1,40 @@
"""
Objects module tests
"""
import unittest
from txtai.pipeline import Objects
# pylint: disable=C0411
from utils import Utils
class TestObjects(unittest.TestCase):
"""
Object detection tests.
"""
def testClassification(self):
"""
Test object detection using an image classification model
"""
objects = Objects(classification=True, threshold=0.3)
self.assertEqual(objects(Utils.PATH + "/books.jpg")[0][0], "library")
def testDetection(self):
"""
Test object detection using an object detection model
"""
objects = Objects()
self.assertEqual(objects(Utils.PATH + "/books.jpg")[0][0], "book")
def testFlatten(self):
"""
Test object detection using an object detection model, flatten to return only objects
"""
objects = Objects()
self.assertEqual(objects(Utils.PATH + "/books.jpg", flatten=True)[0], "book")
@@ -0,0 +1,24 @@
"""
Generator module tests
"""
import unittest
from txtai.pipeline import Generator
class TestGenerator(unittest.TestCase):
"""
Sequences tests.
"""
def testGeneration(self):
"""
Test text pipeline generation
"""
model = Generator("hf-internal-testing/tiny-random-gpt2")
start = "Hello, how are"
# Test that text is generated
self.assertIsNotNone(model(start))
@@ -0,0 +1,115 @@
"""
LiteLLM module tests
"""
import json
import os
import time
import unittest
import uuid
from unittest.mock import patch
from http.server import HTTPServer, BaseHTTPRequestHandler
from threading import Thread
from txtai.pipeline import LLM
class RequestHandler(BaseHTTPRequestHandler):
"""
Test HTTP handler.
"""
def do_POST(self):
"""
POST request handler.
"""
# Parse input headers
length = int(self.headers["content-length"])
data = json.loads(self.rfile.read(length))
if data.get("stream"):
# Mock streaming response
content = "application/octet-stream"
response = (
"data: "
+ json.dumps(
{
"id": str(uuid.uuid4()),
"object": "chat.completion.chunk",
"created": int(time.time() * 1000),
"model": "test",
"choices": [{"id": 0, "delta": {"content": "blue"}}],
}
)
+ "\n\ndata: [DONE]\n\n"
)
else:
# Mock standard response
content = "application/json"
response = json.dumps(
{
"id": str(uuid.uuid4()),
"object": "chat.completion",
"created": int(time.time() * 1000),
"model": "test",
"choices": [{"id": 0, "message": {"role": "assistant", "content": "blue"}, "finish_reason": "stop"}],
}
)
# Encode response as bytes
response = response.encode("utf-8")
self.send_response(200)
self.send_header("content-type", content)
self.send_header("content-length", len(response))
self.end_headers()
self.wfile.write(response)
self.wfile.flush()
class TestLiteLLM(unittest.TestCase):
"""
LiteLLM tests.
"""
@classmethod
def setUpClass(cls):
"""
Create mock http server.
"""
cls.httpd = HTTPServer(("127.0.0.1", 8000), RequestHandler)
server = Thread(target=cls.httpd.serve_forever, daemon=True)
server.start()
@classmethod
def tearDownClass(cls):
"""
Shutdown mock http server.
"""
cls.httpd.shutdown()
@patch.dict(os.environ, {"OPENAI_API_KEY": "test"})
def testGeneration(self):
"""
Test generation with LiteLLM
"""
# Test model generation with LiteLLM
model = LLM("openai/gpt-4o", api_base="http://127.0.0.1:8000")
self.assertEqual(model("The sky is"), "blue")
# Test default role
self.assertEqual(model("The sky is", defaultrole="user"), "blue")
# Test streaming
self.assertEqual(" ".join(x for x in model("The sky is", stream=True)), "blue")
# Test vision
self.assertEqual(model.isvision(), False)
@@ -0,0 +1,31 @@
"""
LiteRT module tests
"""
import unittest
from txtai.pipeline import LLM
class TestLiteRT(unittest.TestCase):
"""
LiteRT tests.
"""
def testGeneration(self):
"""
Test generation with LiteRT
"""
# Test model generation with LiteRT
model = LLM("neuml/gemma-4-tiny-random-litert-lm/gemma-4-tiny-random.litertlm", mtp=False, maxlength=25)
# Test standard
self.assertIsNotNone(model("Hello"))
# Test streaming
self.assertIsNotNone(list(model("Hello", stream=True)))
# Test CPU fallback
model = LLM("neuml/gemma-4-tiny-random-litert-lm/gemma-4-tiny-random.litertlm", mtp=True, maxlength=25)
self.assertIsNotNone(model("Hello"))
@@ -0,0 +1,76 @@
"""
Llama module tests
"""
import unittest
from unittest.mock import patch
from txtai.pipeline import LLM
class TestLlama(unittest.TestCase):
"""
llama.cpp tests.
"""
@patch("llama_cpp.Llama")
def testContext(self, llama):
"""
Test n_ctx with llama.cpp
"""
class Llama:
"""
Mock llama.cpp instance to test invalid context
"""
def __init__(self, **kwargs):
if kwargs.get("n_ctx") == 0 or kwargs.get("n_ctx", 0) >= 10000:
raise ValueError("Failed to create context")
# Save parameters
self.params = kwargs
# Mock llama.cpp instance
llama.side_effect = Llama
# Model to test
path = "TheBloke/TinyLlama-1.1B-Chat-v0.3-GGUF/tinyllama-1.1b-chat-v0.3.Q2_K.gguf"
# Test omitting n_ctx falls back to default settings
llm = LLM(path)
self.assertNotIn("n_ctx", llm.generator.llm.params)
# Test n_ctx=0 falls back to default settings
llm = LLM(path, n_ctx=0)
self.assertNotIn("n_ctx", llm.generator.llm.params)
# Test n_ctx manually set
llm = LLM(path, n_ctx=1024)
self.assertEqual(llm.generator.llm.params["n_ctx"], 1024)
# Mock a value for n_ctx that's too big
with self.assertRaises(ValueError):
llm = LLM(path, n_ctx=10000)
def testGeneration(self):
"""
Test generation with llama.cpp
"""
# Test model generation with llama.cpp
model = LLM("TheBloke/TinyLlama-1.1B-Chat-v0.3-GGUF/tinyllama-1.1b-chat-v0.3.Q2_K.gguf", chat_format="chatml")
# Test with prompt
self.assertEqual(model("2 + 2 = ", maxlength=10, seed=0, stop=["."], defaultrole="prompt")[0], "4")
# Test with list of messages
messages = [{"role": "system", "content": "You are a helpful assistant. You answer math problems."}, {"role": "user", "content": "2+2?"}]
self.assertIsNotNone(model(messages, maxlength=10, seed=0, stop=["."]))
# Test default role
self.assertIsNotNone(model("2 + 2 = ", maxlength=10, seed=0, stop=["."], defaultrole="user"))
# Test streaming
self.assertEqual(" ".join(x for x in model("2 + 2 = ", maxlength=10, stream=True, seed=0, stop=["."], defaultrole="prompt"))[0], "4")
+185
View File
@@ -0,0 +1,185 @@
"""
LLM module tests
"""
import unittest
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from txtai.pipeline import LLM, Generation
# pylint: disable=C0411
from utils import Utils
class TestLLM(unittest.TestCase):
"""
LLM tests.
"""
def testArguments(self):
"""
Test pipeline keyword arguments
"""
start = "Hello, how are"
# Test that text is generated with custom parameters
model = LLM("hf-internal-testing/tiny-random-gpt2", task="language-generation", dtype="torch.float32")
self.assertIsNotNone(model(start))
model = LLM("hf-internal-testing/tiny-random-gpt2", task="language-generation", dtype=torch.float32)
self.assertIsNotNone(model(start))
def testBatchSize(self):
"""
Test batch size
"""
model = LLM("sshleifer/tiny-gpt2")
self.assertIsNotNone(model(["Hello, how are"] * 2, batch_size=2))
def testCustom(self):
"""
Test custom LLM framework
"""
model = LLM("hf-internal-testing/tiny-random-gpt2", task="language-generation", method="txtai.pipeline.HFGeneration")
self.assertIsNotNone(model("Hello, how are"))
def testCustomNotFound(self):
"""
Test resolving an unresolvable LLM framework
"""
with self.assertRaises(ImportError):
LLM("hf-internal-testing/tiny-random-gpt2", method="notfound.generation")
def testDefaultRole(self):
"""
Test default role
"""
model = LLM("hf-internal-testing/tiny-random-LlamaForCausalLM")
generator = model.generator
# Validate that the LLM supports chat messages
self.assertEqual(model.ischat(), True)
messages = [
("Hello", list),
("\n<|im_start|>Hello<|im_end|>", str),
("<|start|>Hello<|end|>", str),
("<|start_of_role|>system<|end_of_role|>", str),
("[INST]Hello[/INST]", str),
]
for message, expected in messages:
# Test auto detection of formats
self.assertEqual(type(generator.format([message], "auto")[0]), expected)
# Test always setting user chat messages
self.assertEqual(type(generator.format([message], "user")[0]), list)
# Test always keeping as prompt text
self.assertEqual(type(generator.format([message], "prompt")[0]), str)
def testExternal(self):
"""
Test externally loaded model
"""
model = AutoModelForCausalLM.from_pretrained("hf-internal-testing/tiny-random-gpt2")
tokenizer = AutoTokenizer.from_pretrained("hf-internal-testing/tiny-random-gpt2")
model = LLM((model, tokenizer), template="{text}")
start = "Hello, how are"
# Test that text is generated
self.assertIsNotNone(model(start))
def testMaxLength(self):
"""
Test max length
"""
model = LLM("sshleifer/tiny-gpt2")
self.assertIsInstance(model("Hello, how are", maxlength=10), str)
def testNotImplemented(self):
"""
Test exceptions for non-implemented methods
"""
generation = Generation()
self.assertRaises(NotImplementedError, generation.stream, None, None, None, None)
def testStop(self):
"""
Test stop strings
"""
model = LLM("sshleifer/tiny-gpt2")
self.assertIsNotNone(model("Hello, how are", stop=["you"]))
def testStream(self):
"""
Test streaming generation
"""
model = LLM("sshleifer/tiny-gpt2")
self.assertIsInstance(" ".join(x for x in model("Hello, how are", stream=True)), str)
def testStripThink(self):
"""
Test stripthink parameter
"""
# pylint: disable=W0613
def execute1(*args, **kwargs):
return ["<think>test</think>you"]
def execute2(*args, **kwargs):
return ["<|channel|>final<|message|> you"]
model = LLM("hf-internal-testing/tiny-random-LlamaForCausalLM")
for method in [execute1, execute2]:
# Override execute method
model.generator.execute = method
self.assertEqual(model("Hello, how are", stripthink=True), "you")
self.assertEqual(model("Hello, how are", stripthink=False), method()[0])
def testStripThinkStream(self):
"""
Test stripthink parameter with streaming output
"""
# pylint: disable=W0613
def execute1(*args, **kwargs):
yield from "<think>test</think>you"
def execute2(*args, **kwargs):
yield from "<|channel|>final<|message|>you"
model = LLM("hf-internal-testing/tiny-random-LlamaForCausalLM")
for method in [execute1, execute2]:
# Override execute method
model.generator.execute = method
self.assertEqual("".join(model("Hello, how are", stripthink=True, stream=True)), "you")
self.assertEqual("".join(model("Hello, how are", stripthink=False, stream=True)), "".join(list(method())))
def testVision(self):
"""
Test vision LLM
"""
model = LLM("neuml/tiny-random-qwen2vl")
result = model(
[{"role": "user", "content": [{"type": "text", "text": "What is in this image?"}, {"type": "image", "image": Utils.PATH + "/books.jpg"}]}]
)
self.assertIsNotNone(result)
@@ -0,0 +1,71 @@
"""
OpenCode module tests
"""
import json
import unittest
from http.server import HTTPServer, BaseHTTPRequestHandler
from threading import Thread
from txtai.pipeline import LLM
class RequestHandler(BaseHTTPRequestHandler):
"""
Test HTTP handler.
"""
def do_POST(self):
"""
POST request handler.
"""
# Mock response
content = "application/json"
response = json.dumps({"id": "0", "parts": [{"type": "text", "text": "blue"}]})
# Encode response as bytes
response = response.encode("utf-8")
self.send_response(200)
self.send_header("content-type", content)
self.send_header("content-length", len(response))
self.end_headers()
self.wfile.write(response)
self.wfile.flush()
class TestOpenCode(unittest.TestCase):
"""
OpenCode tests.
"""
@classmethod
def setUpClass(cls):
"""
Create mock http server.
"""
cls.httpd = HTTPServer(("127.0.0.1", 8005), RequestHandler)
server = Thread(target=cls.httpd.serve_forever, daemon=True)
server.start()
@classmethod
def tearDownClass(cls):
"""
Shutdown mock http server.
"""
cls.httpd.shutdown()
def testGeneration(self):
"""
Test generation with OpenCode
"""
# Test model generation with LiteLLM
model = LLM("opencode/big-pickle", url="http://127.0.0.1:8005")
self.assertEqual(model("The sky is"), "blue")
+225
View File
@@ -0,0 +1,225 @@
"""
RAG module tests
"""
import platform
import unittest
from txtai.embeddings import Embeddings
from txtai.pipeline import Questions, RAG, Similarity
class TestRAG(unittest.TestCase):
"""
RAG tests.
"""
@classmethod
def setUpClass(cls):
"""
Create single rag instance.
"""
cls.data = [
"Giants hit 3 HRs to down Dodgers",
"Giants 5 Dodgers 4 final",
"Dodgers drop Game 2 against the Giants, 5-4",
"Blue Jays beat Red Sox final score 2-1",
"Red Sox lost to the Blue Jays, 2-1",
"Blue Jays at Red Sox is over. Score: 2-1",
"Phillies win over the Braves, 5-0",
"Phillies 5 Braves 0 final",
"Final: Braves lose to the Phillies in the series opener, 5-0",
"Lightning goaltender pulled, lose to Flyers 4-1",
"Flyers 4 Lightning 1 final",
"Flyers win 4-1",
]
# Create embeddings model, backed by sentence-transformers & transformers
cls.embeddings = Embeddings({"path": "sentence-transformers/nli-mpnet-base-v2"})
# Create rag instance
cls.rag = RAG(cls.embeddings, "distilbert-base-cased-distilled-squad")
@classmethod
def tearDownClass(cls):
"""
Cleanup data.
"""
if cls.embeddings:
cls.embeddings.close()
def testAnswer(self):
"""
Test qa extraction with an answer
"""
questions = ["What team won the game?", "What was score?"]
# pylint: disable=C3001
execute = lambda query: self.rag([(question, query, question, False) for question in questions], self.data)
answers = execute("Red Sox - Blue Jays")
self.assertEqual("Blue Jays", answers[0][1])
self.assertEqual("2-1", answers[1][1])
# Ad-hoc questions
question = "What hockey team won?"
answers = self.rag([(question, question, question, False)], self.data)
self.assertEqual("Flyers", answers[0][1])
def testEmptyQuery(self):
"""
Test an empty queries list
"""
self.assertEqual(self.rag.query(None, None), [])
def testNoAnswer(self):
"""
Test qa extraction with no answer
"""
question = ""
answers = self.rag([(question, question, question, False)], self.data)
self.assertIsNone(answers[0][1])
question = "abcdef"
answers = self.rag([(question, question, question, False)], self.data)
self.assertIsNone(answers[0][1])
@unittest.skipIf(platform.system() == "Darwin", "Quantized models not supported on macOS")
def testQuantize(self):
"""
Test qa extraction backed by a quantized model
"""
rag = RAG(self.embeddings, "distilbert-base-cased-distilled-squad", True)
question = "How many home runs?"
answers = rag([(question, question, question, True)], self.data)
self.assertIsNotNone(answers[0][1])
def testOutputs(self):
"""
Test output formatting rules
"""
question = "How many home runs?"
# Test flatten to list of answers
rag = RAG(self.embeddings, "distilbert-base-cased-distilled-squad", output="flatten")
answers = rag([(question, question, question, True)], self.data)
self.assertTrue(answers[0].startswith("Giants hit 3 HRs"))
# Test reference field
rag = RAG(self.embeddings, "distilbert-base-cased-distilled-squad", output="reference")
answers = rag([(question, question, question, True)], self.data)
self.assertTrue(self.data[answers[0][2]].startswith("Giants hit 3 HRs"))
def testPrompt(self):
"""
Test a user prompt with templating
"""
embeddings = Embeddings({"path": "sentence-transformers/nli-mpnet-base-v2", "content": True})
embeddings.index([(uid, text, None) for uid, text in enumerate(self.data)])
rag = RAG(
embeddings,
"google/flan-t5-small",
template="""
Answer the following question and return a number.
Question: {question}
Context:{context}""",
output="flatten",
)
self.assertEqual(rag("How many HRs"), "3")
def testPromptTemplates(self):
"""
Test system and user prompt templates
"""
rag = RAG(
self.embeddings,
"sshleifer/tiny-gpt2",
system="You are a friendly assistant",
template="""
Answer the following question and return a number.
Question: {question}
Context:{context}""",
)
prompts = rag.prompts(["How many HRs?"], [self.data])[0]
self.assertEqual([x["role"] for x in prompts], ["system", "user"])
def testSearch(self):
"""
Test qa extraction with an embeddings search for context
"""
embeddings = Embeddings({"path": "sentence-transformers/nli-mpnet-base-v2", "content": True})
embeddings.index([(uid, text, None) for uid, text in enumerate(self.data)])
rag = RAG(embeddings, "distilbert-base-cased-distilled-squad")
question = "How many home runs?"
answers = rag([(question, question, question, True)])
self.assertTrue(answers[0][1].startswith("Giants hit 3 HRs"))
def testSimilarity(self):
"""
Test qa extraction using a Similarity pipeline to build context
"""
# Create rag instance
rag = RAG(Similarity("prajjwal1/bert-medium-mnli"), Questions("distilbert-base-cased-distilled-squad"))
question = "How many home runs?"
answers = rag([(question, "HRs", question, True)], self.data)
self.assertTrue(answers[0][1].startswith("Giants hit 3 HRs"))
def testSnippet(self):
"""
Test qa extraction with a full answer snippet
"""
question = "How many home runs?"
answers = self.rag([(question, question, question, True)], self.data)
self.assertTrue(answers[0][1].startswith("Giants hit 3 HRs"))
def testSnippetEmpty(self):
"""
Test snippet method can handle empty parameters
"""
self.assertEqual(self.rag.snippets(["name"], [None], [None], [None]), [("name", None)])
def testStringInput(self):
"""
Test with single string input
"""
result = self.rag("How many home runs?", self.data)
self.assertEqual(result["answer"], "3")
def testTasks(self):
"""
Test loading models with task parameter
"""
for task, model in [
("language-generation", "hf-internal-testing/tiny-random-gpt2"),
("sequence-sequence", "hf-internal-testing/tiny-random-t5"),
]:
rag = RAG(self.embeddings, model, task=task)
self.assertIsNotNone(rag)
@@ -0,0 +1,21 @@
"""
Sequences module tests
"""
import unittest
from txtai.pipeline import Sequences
class TestSequences(unittest.TestCase):
"""
Sequences tests.
"""
def testGeneration(self):
"""
Test text2text pipeline generation
"""
model = Sequences("t5-small")
self.assertEqual(model("Testing the model", prefix="translate English to German: "), "Das Modell zu testen")
@@ -0,0 +1,63 @@
"""
Entity module tests
"""
import unittest
from txtai.pipeline import Entity
class TestEntity(unittest.TestCase):
"""
Entity tests.
"""
@classmethod
def setUpClass(cls):
"""
Create entity instance.
"""
cls.entity = Entity("dslim/bert-base-NER")
def testEntity(self):
"""
Test entity
"""
# Run entity extraction
entities = self.entity("Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg")
self.assertEqual([e[0] for e in entities], ["Canada", "Manhattan"])
def testEntityFlatten(self):
"""
Test entity with flattened output
"""
# Test flatten
entities = self.entity("Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg", flatten=True)
self.assertEqual(entities, ["Canada", "Manhattan"])
# Test flatten with join
entities = self.entity(
"Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg", flatten=True, join=True
)
self.assertEqual(entities, "Canada Manhattan")
def testEntityTypes(self):
"""
Test entity type filtering
"""
# Run entity extraction
entities = self.entity("Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg", labels=["PER"])
self.assertFalse(entities)
def testGliner(self):
"""
Test entity pipeline with a GLiNER model
"""
entity = Entity("neuml/gliner-bert-tiny")
entities = entity("My name is John Smith.", flatten=True)
self.assertEqual(entities, ["John Smith"])
@@ -0,0 +1,85 @@
"""
Labels module tests
"""
import unittest
from txtai.pipeline import Labels
class TestLabels(unittest.TestCase):
"""
Labels tests.
"""
@classmethod
def setUpClass(cls):
"""
Create single labels instance.
"""
cls.data = [
"US tops 5 million confirmed virus cases",
"Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg",
"Beijing mobilises invasion craft along coast as Taiwan tensions escalate",
"The National Park Service warns against sacrificing slower friends in a bear attack",
"Maine man wins $1M from $25 lottery ticket",
"Make huge profits without work, earn up to $100,000 a day",
]
cls.labels = Labels("prajjwal1/bert-medium-mnli")
def testLabel(self):
"""
Test labels with single text input
"""
self.assertEqual(self.labels("This is the best sentence ever", ["positive", "negative"])[0][0], 0)
def testLabelFlatten(self):
"""
Test labels with single text input, flattened to top text labels
"""
self.assertEqual(self.labels("This is the best sentence ever", ["positive", "negative"], flatten=True)[0], "positive")
def testLabelBatch(self):
"""
Test labels with multiple text inputs
"""
results = [l[0][0] for l in self.labels(["This is the best sentence ever", "This is terrible"], ["positive", "negative"])]
self.assertEqual(results, [0, 1])
def testLabelBatchFlatten(self):
"""
Test labels with multiple text inputs, flattened to top text labels
"""
results = [l[0] for l in self.labels(["This is the best sentence ever", "This is terrible"], ["positive", "negative"], flatten=True)]
self.assertEqual(results, ["positive", "negative"])
def testLabelFixed(self):
"""
Test labels with a fixed label text classification model
"""
labels = Labels(dynamic=False)
# Get index of "POSITIVE" label
index = labels.labels().index("POSITIVE")
# Verify results
self.assertEqual(labels("This is the best sentence ever")[0][0], index)
self.assertEqual(labels("This is the best sentence ever", multilabel=True)[0][0], index)
def testLabelFixedFlatten(self):
"""
Test labels with a fixed label text classification model, flattened to top text labels
"""
labels = Labels(dynamic=False)
# Verify results
self.assertEqual(labels("This is the best sentence ever", flatten=True)[0], "POSITIVE")
self.assertEqual(labels("This is the best sentence ever", multilabel=True, flatten=True)[0], "POSITIVE")
@@ -0,0 +1,42 @@
"""
Reranker module tests
"""
import unittest
from txtai import Embeddings
from txtai.pipeline import Reranker, Similarity
class TestReranker(unittest.TestCase):
"""
Reranker tests.
"""
@classmethod
def setUpClass(cls):
"""
Create single labels instance.
"""
cls.data = [
"US tops 5 million confirmed virus cases",
"Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg",
"Beijing mobilises invasion craft along coast as Taiwan tensions escalate",
"The National Park Service warns against sacrificing slower friends in a bear attack",
"Maine man wins $1M from $25 lottery ticket",
"Make huge profits without work, earn up to $100,000 a day",
]
def testRanker(self):
"""
Test re-ranking pipeline
"""
embeddings = Embeddings(content=True)
embeddings.index(self.data)
similarity = Similarity("neuml/colbert-bert-tiny", lateencode=True)
ranker = Reranker(embeddings, similarity)
self.assertEqual(ranker("lottery winner")[0]["id"], "4")
@@ -0,0 +1,105 @@
"""
Similarity module tests
"""
import unittest
from txtai.pipeline import Similarity
class TestSimilarity(unittest.TestCase):
"""
Similarity tests.
"""
@classmethod
def setUpClass(cls):
"""
Create single labels instance.
"""
cls.data = [
"US tops 5 million confirmed virus cases",
"Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg",
"Beijing mobilises invasion craft along coast as Taiwan tensions escalate",
"The National Park Service warns against sacrificing slower friends in a bear attack",
"Maine man wins $1M from $25 lottery ticket",
"Make huge profits without work, earn up to $100,000 a day",
]
cls.similarity = Similarity("prajjwal1/bert-medium-mnli")
def testCrossEncoder(self):
"""
Test cross-encoder similarity model
"""
similarity = Similarity("cross-encoder/ms-marco-MiniLM-L-2-v2", crossencode=True)
uid = similarity("Who won the lottery?", self.data)[0][0]
self.assertEqual(self.data[uid], self.data[4])
def testCrossEncoderBatch(self):
"""
Test cross-encoder similarity model with multiple inputs
"""
similarity = Similarity("cross-encoder/ms-marco-MiniLM-L-2-v2", crossencode=True)
results = [r[0][0] for r in similarity(["Who won the lottery?", "Where did an iceberg collapse?"], self.data)]
self.assertEqual(results, [4, 1])
def testLateEncoder(self):
"""
Test late-encoder similarity model
"""
similarity = Similarity("neuml/pylate-bert-tiny", lateencode=True)
uid = similarity("Who won the lottery?", self.data)[0][0]
self.assertEqual(self.data[uid], self.data[4])
# Test encode method
# pylint: disable=E1101
self.assertEqual(similarity.encode(["Who won the lottery?"], "data").shape, (1, 8, 128))
def testLateEncoderBatch(self):
"""
Test late-encoder similarity model with multiple inputs
"""
similarity = Similarity("neuml/colbert-bert-tiny", lateencode=True)
results = [r[0][0] for r in similarity(["Who won the lottery?", "Where did an iceberg collapse?"], self.data)]
self.assertEqual(results, [4, 1])
def testSimilarity(self):
"""
Test similarity with single query
"""
uid = self.similarity("feel good story", self.data)[0][0]
self.assertEqual(self.data[uid], self.data[4])
def testSimilarityBatch(self):
"""
Test similarity with multiple queries
"""
results = [r[0][0] for r in self.similarity(["feel good story", "climate change"], self.data)]
self.assertEqual(results, [4, 1])
def testSimilarityFixed(self):
"""
Test similarity with a fixed label text classification model
"""
similarity = Similarity(dynamic=False)
# Test with query as label text and label id
self.assertLessEqual(similarity("negative", ["This is the best sentence ever"])[0][1], 0.1)
self.assertLessEqual(similarity("0", ["This is the best sentence ever"])[0][1], 0.1)
def testSimilarityLong(self):
"""
Test similarity with long text
"""
uid = self.similarity("other", ["Very long text " * 1000, "other text"])[0][0]
self.assertEqual(uid, 1)
@@ -0,0 +1,64 @@
"""
Summary module tests
"""
import unittest
from txtai.pipeline import Summary
class TestSummary(unittest.TestCase):
"""
Summary tests.
"""
@classmethod
def setUpClass(cls):
"""
Create single summary instance.
"""
cls.text = (
"Search is the base of many applications. Once data starts to pile up, users want to be able to find it. It's the foundation "
"of the internet and an ever-growing challenge that is never solved or done. The field of Natural Language Processing (NLP) is "
"rapidly evolving with a number of new developments. Large-scale general language models are an exciting new capability "
"allowing us to add amazing functionality quickly with limited compute and people. Innovation continues with new models "
"and advancements coming in at what seems a weekly basis. This article introduces txtai, an AI-powered search engine "
"that enables Natural Language Understanding (NLU) based search in any application."
)
cls.summary = Summary("t5-small")
def testSummary(self):
"""
Test summarization of text
"""
self.assertEqual(self.summary(self.text, minlength=15, maxlength=15), "the field of natural language processing (NLP) is rapidly evolving")
def testSummaryBatch(self):
"""
Test batch summarization of text
"""
summaries = self.summary([self.text, self.text], maxlength=15)
self.assertEqual(len(summaries), 2)
def testSummaryNoLength(self):
"""
Test summary with no max length set
"""
self.assertEqual(
self.summary(self.text + self.text),
"search is the base of many applications. Once data starts to pile up, users want to be able to find it. "
+ "Large-scale general language models are an exciting new capability allowing us to add amazing functionality quickly "
+ "with limited compute and people.",
)
def testSummaryShort(self):
"""
Test that summarization is skipped
"""
self.assertEqual(self.summary("Text", maxlength=15), "Text")
@@ -0,0 +1,175 @@
"""
Translation module tests
"""
import unittest
import time
import requests
from txtai.pipeline import Translation
class TestTranslation(unittest.TestCase):
"""
Translation tests.
"""
@classmethod
def setUpClass(cls):
"""
Create single translation instance.
"""
cls.translate = Translation()
# Preload list of models. Handle HF Hub errors.
complete, wait = False, 1
while not complete:
try:
cls.translate.lookup("en", "es")
complete = True
except requests.exceptions.HTTPError:
# Exponential backoff
time.sleep(wait)
# Wait up to 16 seconds
wait = min(wait * 2, 16)
def testDetect(self):
"""
Test language detection
"""
test = ["This is a test language detection."]
language = self.translate.detect(test)
self.assertListEqual(language, ["en"])
def testDetectWithCustomFunc(self):
"""
Test language detection with custom function
"""
def dummy_func(text):
return ["en" for x in text]
translate = Translation(langdetect=dummy_func)
test = ["This is a test language detection."]
language = translate.detect(test)
self.assertListEqual(language, ["en"])
def testLongTranslation(self):
"""
Test a translation longer than max tokenization length
"""
text = "This is a test translation to Spanish. " * 100
translation = self.translate(text, "es")
# Validate translation text
self.assertIsNotNone(translation)
def testM2M100Translation(self):
"""
Test a translation using M2M100 models
"""
text = self.translate("This is a test translation to Croatian", "hr")
# Validate translation text
self.assertEqual(text, "Ovo je testni prijevod na hrvatski")
def testMarianTranslation(self):
"""
Test a translation using Marian models
"""
text = "This is a test translation into Spanish"
translation = self.translate(text, "es")
# Validate translation text
self.assertEqual(translation, "Esta es una traducción de prueba al español")
# Validate translation back
translation = self.translate(translation, "en")
self.assertEqual(translation, text)
def testNoLang(self):
"""
Test no matching language id
"""
self.assertIsNone(self.translate.langid([], "zz"))
def testNoModel(self):
"""
Test no known available model found
"""
self.assertEqual(self.translate.modelpath("zz", "en"), "Helsinki-NLP/opus-mt-mul-en")
def testNoTranslation(self):
"""
Test translation skipped when text already in destination language
"""
text = "This is a test translation to English"
translation = self.translate(text, "en")
# Validate no translation
self.assertEqual(text, translation)
def testShowmodelsChunked(self):
"""
Test a long translation with showmodels flag. When text is chunked
by the tokenizer, results should still be properly concatenated as
a 3-tuple (translation, language, model) rather than a malformed tuple.
"""
text = "This is a test translation to Spanish. " * 100
result = self.translate(text, "es", showmodels=True)
# Result should be a tuple of exactly 3 elements
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 3)
translation, language, modelpath = result
# Translation should be a single string, not a nested tuple
self.assertIsInstance(translation, str)
self.assertIsNotNone(translation)
self.assertGreater(len(translation), 0)
# Language and model should be valid strings
self.assertEqual(language, "en")
self.assertIsInstance(modelpath, str)
def testTranslationWithShowmodels(self):
"""
Test a translation using Marian models and showmodels flag to return
model and language.
"""
text = "This is a test translation into Spanish"
result = self.translate(text, "es", showmodels=True)
translation, language, modelpath = result
# Validate translation text
self.assertEqual(translation, "Esta es una traducción de prueba al español")
# Validate detected language
self.assertEqual(language, "en")
# Validate model
self.assertEqual(modelpath, "Helsinki-NLP/opus-mt-en-es")
# Validate translation back
result = self.translate(translation, "en", showmodels=True)
translation, language, modelpath = result
self.assertEqual(translation, text)
# Validate detected language
self.assertEqual(language, "es")
# Validate model
self.assertEqual(modelpath, "Helsinki-NLP/opus-mt-es-en")
@@ -0,0 +1,161 @@
"""
ONNX module tests
"""
import os
import tempfile
import unittest
from unittest.mock import patch
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from txtai.embeddings import Embeddings
from txtai.models import OnnxModel
from txtai.pipeline import HFOnnx, HFTrainer, Labels, MLOnnx, Questions
class TestOnnx(unittest.TestCase):
"""
ONNX tests.
"""
@classmethod
def setUpClass(cls):
"""
Create default datasets.
"""
cls.data = [{"text": "Dogs", "label": 0}, {"text": "dog", "label": 0}, {"text": "Cats", "label": 1}, {"text": "cat", "label": 1}] * 100
def testDefault(self):
"""
Test exporting an ONNX model with default parameters
"""
# Export model to ONNX, use default parameters
onnx = HFOnnx()
model = onnx("google/bert_uncased_L-2_H-128_A-2")
# Validate model has data
self.assertGreater(len(model), 0)
# Validate model device properly works
self.assertEqual(OnnxModel(model).device, -1)
def testClassification(self):
"""
Test exporting a classification model to ONNX and running inference
"""
path = "google/bert_uncased_L-2_H-128_A-2"
trainer = HFTrainer()
model, tokenizer = trainer(path, self.data)
# Output file path
output = os.path.join(tempfile.gettempdir(), "onnx")
# Export model to ONNX
onnx = HFOnnx()
model = onnx((model, tokenizer), "text-classification", output, True)
# Test classification
labels = Labels((model, path), dynamic=False)
self.assertEqual(labels("cat")[0][0], 1)
@patch("onnxruntime.get_available_providers")
@patch("torch.cuda.is_available")
def testPooling(self, cuda, providers):
"""
Test exporting a pooling model to ONNX and running inference
"""
path = "sentence-transformers/paraphrase-MiniLM-L3-v2"
# Export model to ONNX
onnx = HFOnnx()
model = onnx(path, "pooling", quantize=True)
# Test no CUDA and onnxruntime installed
cuda.return_value = False
providers.return_value = ["CPUExecutionProvider"]
embeddings = Embeddings({"path": model, "tokenizer": path})
self.assertEqual(embeddings.similarity("animal", ["dog", "book", "rug"])[0][0], 0)
# Test no CUDA and onnxruntime-gpu installed
cuda.return_value = False
providers.return_value = ["CUDAExecutionProvider", "CPUExecutionProvider"]
embeddings = Embeddings({"path": model, "tokenizer": path})
self.assertIsNotNone(embeddings)
# Test CUDA and only onnxruntime installed
cuda.return_value = True
providers.return_value = ["CPUExecutionProvider"]
embeddings = Embeddings({"path": model, "tokenizer": path})
self.assertIsNotNone(embeddings)
# Test CUDA and onnxruntime-gpu installed
cuda.return_value = True
providers.return_value = ["CUDAExecutionProvider", "CPUExecutionProvider"]
embeddings = Embeddings({"path": model, "tokenizer": path})
self.assertIsNotNone(embeddings)
def testQA(self):
"""
Test exporting a QA model to ONNX and running inference
"""
path = "distilbert-base-cased-distilled-squad"
# Export model to ONNX
onnx = HFOnnx()
model = onnx(path, "question-answering")
questions = Questions((model, path))
self.assertEqual(questions(["What is the price?"], ["The price is $30"])[0], "$30")
def testScikit(self):
"""
Test exporting a scikit-learn model to ONNX and running inference
"""
# pylint: disable=W0613
def tokenizer(inputs, **kwargs):
if isinstance(inputs, str):
inputs = [inputs]
return {"input_ids": [[x] for x in inputs]}
# Train a scikit-learn model
model = Pipeline([("tfidf", TfidfVectorizer()), ("lr", LogisticRegression())])
model.fit([x["text"] for x in self.data], [x["label"] for x in self.data])
# Export model to ONNX
onnx = MLOnnx()
model = onnx(model)
# Test classification
labels = Labels((model, tokenizer), dynamic=False)
self.assertEqual(labels("cat")[0][0], 1)
def testZeroShot(self):
"""
Test exporting a zero shot classification model to ONNX and running inference
"""
path = "prajjwal1/bert-medium-mnli"
# Export model to ONNX
onnx = HFOnnx()
model = onnx(path, "zero-shot-classification", quantize=True)
# Test zero shot classification
labels = Labels((model, path))
self.assertEqual(labels("That is great news", ["negative", "positive"])[0][0], 1)
@@ -0,0 +1,35 @@
"""
Quantization module tests
"""
import platform
import unittest
from transformers import AutoModel
from txtai.pipeline import HFModel, HFPipeline
class TestQuantization(unittest.TestCase):
"""
Quantization tests.
"""
@unittest.skipIf(platform.system() == "Darwin", "Quantized models not supported on macOS")
def testModel(self):
"""
Test quantizing a model through HFModel.
"""
model = HFModel(quantize=True, gpu=False)
model = model.prepare(AutoModel.from_pretrained("google/bert_uncased_L-2_H-128_A-2"))
self.assertIsNotNone(model)
@unittest.skipIf(platform.system() == "Darwin", "Quantized models not supported on macOS")
def testPipeline(self):
"""
Test quantizing a model through HFPipeline.
"""
pipeline = HFPipeline("text-classification", "google/bert_uncased_L-2_H-128_A-2", True, False)
self.assertIsNotNone(pipeline)
@@ -0,0 +1,335 @@
"""
Trainer module tests
"""
import os
import unittest
import tempfile
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from txtai.data import Data
from txtai.pipeline import HFTrainer, Labels, Questions, Sequences
class TestTrainer(unittest.TestCase):
"""
Trainer tests.
"""
@classmethod
def setUpClass(cls):
"""
Create default datasets.
"""
cls.data = [{"text": "Dogs", "label": 0}, {"text": "dog", "label": 0}, {"text": "Cats", "label": 1}, {"text": "cat", "label": 1}] * 100
def testBasic(self):
"""
Test training a model with basic parameters
"""
trainer = HFTrainer()
model, tokenizer = trainer("google/bert_uncased_L-2_H-128_A-2", self.data)
labels = Labels((model, tokenizer), dynamic=False)
self.assertEqual(labels("cat")[0][0], 1)
def testCLM(self):
"""
Test training a model with causal language modeling
"""
trainer = HFTrainer()
# Test default parameters
model, _ = trainer("hf-internal-testing/tiny-random-gpt2", self.data, maxlength=16, task="language-generation")
self.assertIsNotNone(model)
# Test pack merging
model, _ = trainer("hf-internal-testing/tiny-random-gpt2", self.data, maxlength=16, task="language-generation", merge="pack")
self.assertIsNotNone(model)
# Test no merging
model, _ = trainer("hf-internal-testing/tiny-random-gpt2", self.data, maxlength=16, task="language-generation", merge=None)
self.assertIsNotNone(model)
def testCustom(self):
"""
Test training a model with custom parameters
"""
# pylint: disable=E1120
model = AutoModelForSequenceClassification.from_pretrained("google/bert_uncased_L-2_H-128_A-2")
tokenizer = AutoTokenizer.from_pretrained("google/bert_uncased_L-2_H-128_A-2")
trainer = HFTrainer()
model, tokenizer = trainer(
(model, tokenizer),
self.data,
self.data,
columns=("text", "label"),
do_eval=True,
output_dir=os.path.join(tempfile.gettempdir(), "trainer"),
)
labels = Labels((model, tokenizer), dynamic=False)
self.assertEqual(labels("cat")[0][0], 1)
def testDataFrame(self):
"""
Test training a model with a mock pandas DataFrame
"""
class TestDataFrame:
"""
Test DataFrame
"""
def __init__(self, data):
# Get list of columns
self.columns = list(data[0].keys())
# Build columnar data view
self.data = {}
for column in self.columns:
self.data[column] = Values([row[column] for row in data])
def __getitem__(self, column):
return self.data[column]
class Values:
"""
Test values list
"""
def __init__(self, values):
self.values = list(values)
def __getitem__(self, index):
return self.values[index]
def unique(self):
"""
Returns a list of unique values.
Returns:
unique list of values
"""
return set(self.values)
# Mock DataFrame
df = TestDataFrame(self.data)
trainer = HFTrainer()
model, tokenizer = trainer("google/bert_uncased_L-2_H-128_A-2", df)
labels = Labels((model, tokenizer), dynamic=False)
self.assertEqual(labels("cat")[0][0], 1)
def testDataset(self):
"""
Test training a model with a mock Hugging Face Dataset
"""
class TestDataset(torch.utils.data.Dataset):
"""
Test Dataset
"""
def __init__(self, data):
self.data = data
self.unique = lambda _: [0, 1]
def __len__(self):
return len(self.data)
def __getitem__(self, index):
return self.data[index]
def column_names(self):
"""
Returns column names for this dataset
Returns:
list of columns
"""
return ["text", "label"]
# pylint: disable=W0613
def map(self, fn, batched, num_proc, remove_columns):
"""
Map each dataset row using fn.
Args:
fn: function
batched: batch records
Returns:
updated Dataset
"""
self.data = [fn(x) for x in self.data]
return self
ds = TestDataset(self.data)
trainer = HFTrainer()
model, tokenizer = trainer("google/bert_uncased_L-2_H-128_A-2", ds)
labels = Labels((model, tokenizer), dynamic=False)
self.assertEqual(labels("cat")[0][0], 1)
def testEmpty(self):
"""
Test an empty training data object
"""
self.assertIsNone(Data(None, None, None).process(None))
def testKD(self):
"""
Test knowledge distillation
"""
# Base model
trainer = HFTrainer()
model, tokenizer = trainer("google/bert_uncased_L-2_H-128_A-2", self.data)
# Train with knowledge distillation
model, tokenizer = trainer("google/bert_uncased_L-2_H-128_A-2", self.data, teacher=(model, tokenizer))
labels = Labels((model, tokenizer), dynamic=False)
self.assertEqual(labels("cat")[0][0], 1)
def testMLM(self):
"""
Test training a model with masked language modeling.
"""
trainer = HFTrainer()
model, _ = trainer("hf-internal-testing/tiny-random-bert", self.data, task="language-modeling")
# Test model completed successfully
self.assertIsNotNone(model)
def testMultiLabel(self):
"""
Test training model with labels provided as a list
"""
data = []
for x in self.data:
data.append({"text": x["text"], "label": [0.0, 1.0] if x["label"] else [1.0, 0.0]})
trainer = HFTrainer()
model, tokenizer = trainer("google/bert_uncased_L-2_H-128_A-2", data)
labels = Labels((model, tokenizer), dynamic=False)
self.assertEqual(labels("cat")[0][0], 1)
def testPEFT(self):
"""
Test training a model with causal language modeling and PEFT
"""
trainer = HFTrainer()
model, _ = trainer(
"hf-internal-testing/tiny-random-gpt2",
self.data,
maxlength=16,
task="language-generation",
quantize=True,
lora=True,
)
# Test model completed successfully
self.assertIsNotNone(model)
def testQA(self):
"""
Test training a QA model
"""
# Training data
data = [
{"question": "What ingredient?", "context": "1 can whole tomatoes", "answers": "tomatoes"},
{"question": "What ingredient?", "context": "Crush 1 tomato", "answers": "tomato"},
{"question": "What ingredient?", "context": "1 yellow onion", "answers": "onion"},
{"question": "What ingredient?", "context": "Unwrap 2 red onions", "answers": "onions"},
{"question": "What ingredient?", "context": "1 red pepper", "answers": "pepper"},
{"question": "What ingredient?", "context": "Clean 3 red peppers", "answers": "peppers"},
{"question": "What ingredient?", "context": "1 clove garlic", "answers": "garlic"},
{"question": "What ingredient?", "context": "Unwrap 3 cloves of garlic", "answers": "garlic"},
{"question": "What ingredient?", "context": "3 pieces of ginger", "answers": "ginger"},
{"question": "What ingredient?", "context": "Peel 1 orange", "answers": "orange"},
{"question": "What ingredient?", "context": "1/2 lb beef", "answers": "beef"},
{"question": "What ingredient?", "context": "Roast 3 lbs of beef", "answers": "beef"},
{"question": "What ingredient?", "context": "1 pack of chicken", "answers": "chicken"},
{"question": "What ingredient?", "context": "Forest through the trees", "answers": None},
]
trainer = HFTrainer()
model, tokenizer = trainer("google/bert_uncased_L-2_H-128_A-2", data, data, task="question-answering", num_train_epochs=40)
questions = Questions((model, tokenizer), gpu=True)
self.assertTrue("onion" in questions(["What ingredient?"], ["Peel 1 onion"])[0])
def testRegression(self):
"""
Test training a model with a regression (continuous) output
"""
data = []
for x in self.data:
data.append({"text": x["text"], "label": x["label"] + 0.1})
trainer = HFTrainer()
model, tokenizer = trainer("google/bert_uncased_L-2_H-128_A-2", data)
labels = Labels((model, tokenizer), dynamic=False)
# Regression tasks return a single entry with the regression output
self.assertGreater(labels("cat")[0][1], 0.5)
def testRTD(self):
"""
Test training a language model with replaced token detection
"""
# Save directory
output = os.path.join(tempfile.gettempdir(), "trainer.rtd")
trainer = HFTrainer()
model, _ = trainer("hf-internal-testing/tiny-random-electra", self.data, task="token-detection", output_dir=output)
# Test model completed successfully
self.assertIsNotNone(model)
# Test output directories exist
self.assertTrue(os.path.exists(os.path.join(output, "generator")))
self.assertTrue(os.path.exists(os.path.join(output, "discriminator")))
def testSeqSeq(self):
"""
Test training a sequence-sequence model
"""
data = [
{"source": "Running again", "target": "Sleeping again"},
{"source": "Run", "target": "Sleep"},
{"source": "running", "target": "sleeping"},
]
trainer = HFTrainer()
model, tokenizer = trainer("t5-small", data, task="sequence-sequence", prefix="translate Run to Sleep: ", learning_rate=1e-3)
# Run run-sleep translation
sequences = Sequences((model, tokenizer))
result = sequences("translate Run to Sleep: run")
self.assertEqual(result.lower(), "sleep")