From 5c321a5e89e8abdbde0a38acdcb371f48ea1b819 Mon Sep 17 00:00:00 2001 From: Kai Christensen Date: Tue, 27 Oct 2026 11:44:20 -0600 Subject: [PATCH 1/7] Add packaging for custom OWLv2 uploads Co-authored-by: OpenAI Codex --- README.md | 2 + docs/owlv2-upload.md | 54 +++++++++++++++++++++ roboflow/util/model_processor.py | 77 +++++++++++++++++++++++++++++- tests/util/test_model_processor.py | 41 ++++++++++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 docs/owlv2-upload.md diff --git a/README.md b/README.md index 1be0577f..98d79be1 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,8 @@ Within the project object, you can retrieve metadata about the project, list ver Within the version object, you can download the dataset version in any model format, train the version on Roboflow, and deploy your own external model to Roboflow. +For custom OWLv2 checkpoints and Auto Label, see [Upload a custom OWLv2 model](docs/owlv2-upload.md). + ## 🏆 Contributing We would love your input on how we can improve the Roboflow Python package! Please see our [contributing guide](https://github.com/roboflow/roboflow-python/blob/main/CONTRIBUTING.md) to get started. Thank you 🙏 to all our contributors! diff --git a/docs/owlv2-upload.md b/docs/owlv2-upload.md new file mode 100644 index 00000000..58782352 --- /dev/null +++ b/docs/owlv2-upload.md @@ -0,0 +1,54 @@ +# Upload a custom OWLv2 model + +Use `model_type="owlv2-custom"` to upload a complete OWLv2 parameter dictionary. +This model uses saved text prompts and returns object detections with fixed labels. +It works through the custom-model Auto Label path and the Object Detection Model Workflow block. + +## Prepare the upload folder + +Save these files beside the checkpoint: + +| File | Content | +| --- | --- | +| `owlv2.pth` | The complete `Owlv2ForObjectDetection.state_dict()`, including the text encoder and detection heads | +| `config.json` | The original model configuration, saved with `model.config.save_pretrained(folder)` | +| Processor and tokenizer files | The original processor, saved with `processor.save_pretrained(folder)` | +| `class_names.txt` | Unique output labels, one per line | +| `owlv2_prompts.json` | Optional JSON array of text prompts, in label order | + +The processor files must include `tokenizer_config.json` and either `tokenizer.json` or both `vocab.json` and `merges.txt`. +Include either `processor_config.json` or `preprocessor_config.json` for image processing. + +If you omit `owlv2_prompts.json`, the model uses each label as its text prompt. +For example, labels `dent` and `scratch` can use prompts `["a dent in a metal panel", "a scratch in a metal panel"]`. +The class IDs follow the order in `class_names.txt`. + +The checkpoint alone does not identify the original preprocessing or text prompts. +Obtain those values from the training or inference code. +The exporter rejects partial checkpoints and adapter-only weights. +The SDK packages files without loading the checkpoint. + +## Upload + +Use an object-detection project that the workspace can access. +Replace the workspace, project, folder, and model name with your values. +Set `ROBOFLOW_API_KEY` in your environment before you run this example. + +```python +from roboflow import Roboflow + +workspace = Roboflow().workspace("your-workspace") +workspace.deploy_model( + model_type="owlv2-custom", + model_path="/absolute/path/to/weights-upload", + filename="owlv2.pth", + project_ids=["your-project"], + model_name="custom-owlv2", +) +``` + +After upload processing finishes, select the uploaded model under custom models in Auto Label. +In Workflows, select its model ID in an Object Detection Model block. +The saved prompts stay fixed for each model package. + +This feature requires the corresponding platform, rflm exporter, and inference runtime releases. diff --git a/roboflow/util/model_processor.py b/roboflow/util/model_processor.py index 48fc85be..a16bb605 100644 --- a/roboflow/util/model_processor.py +++ b/roboflow/util/model_processor.py @@ -68,6 +68,7 @@ "paligemma2", "florence-2", "rfdetr", + "owlv2-custom", ) SUPPORTED_HUGGINGFACE_TYPES = ( @@ -462,7 +463,11 @@ def _process_model( f"It must start with a supported family: {', '.join(SUPPORTED_MODELS)}." ) - if model_type.startswith(("paligemma", "paligemma2", "florence-2")): + if model_type == "owlv2-custom": + return _process_owlv2(model_path, filename, build_dir) + elif model_type.startswith("owlv2-custom"): + raise UnsupportedModelError("Use model_type='owlv2-custom' for OWLv2 checkpoints.") + elif model_type.startswith(("paligemma", "paligemma2", "florence-2")): return _process_huggingface(model_type, model_path, build_dir) if model_type.startswith("yolonas"): return _process_yolonas(model_type, model_path, filename, build_dir) @@ -1125,6 +1130,76 @@ def _process_rfdetr( return archive_path, model_type, warnings +def _process_owlv2(model_path: Path, filename: str, build_dir: Path) -> tuple[Path, str, list[str]]: + required = {"config.json", "tokenizer_config.json", "class_names.txt"} + optional = { + "tokenizer.json", + "vocab.json", + "merges.txt", + "special_tokens_map.json", + "added_tokens.json", + "preprocessor_config.json", + "processor_config.json", + "owlv2_prompts.json", + } + files = {name: _resolve_within_source(model_path, name) for name in required | optional} + missing = sorted(name for name in required if not files[name].is_file()) + if missing: + raise MissingFileError( + f"Missing OWLv2 upload files: {', '.join(missing)}. " + "Export the original model configuration and processor, and provide class_names.txt." + ) + if not files["tokenizer.json"].is_file() and not all( + files[name].is_file() for name in ("vocab.json", "merges.txt") + ): + raise MissingFileError( + "Provide tokenizer.json or both vocab.json and merges.txt from the original OWLv2 processor." + ) + if not any(files[name].is_file() for name in ("preprocessor_config.json", "processor_config.json")): + raise MissingFileError( + "Provide preprocessor_config.json or processor_config.json from the original OWLv2 processor." + ) + try: + config = json.loads(files["config.json"].read_text()) + class_names = files["class_names.txt"].read_text().splitlines() + prompts = ( + json.loads(files["owlv2_prompts.json"].read_text()) + if files["owlv2_prompts.json"].is_file() + else class_names + ) + except (ValueError, UnicodeError) as error: + raise ModelPackagingError(f"Invalid OWLv2 metadata: {error}") from error + if not isinstance(config, dict) or config.get("model_type") != "owlv2": + raise ModelPackagingError("config.json must describe an OWLv2 model (model_type='owlv2').") + if ( + not class_names + or any(not name.strip() or name != name.strip() for name in class_names) + or len(set(class_names)) != len(class_names) + ): + raise ModelPackagingError( + "class_names.txt must contain unique, nonblank labels, one per line, without surrounding spaces." + ) + if ( + not isinstance(prompts, list) + or len(prompts) != len(class_names) + or any(not isinstance(prompt, str) or not prompt.strip() for prompt in prompts) + ): + raise ModelPackagingError( + "owlv2_prompts.json must contain one nonblank text prompt per class, in class_names.txt order." + ) + weights = _resolve_within_source(model_path, filename) + if not weights.is_file(): + raise MissingFileError( + f"OWLv2 weights file '{weights}' was not found. Set filename to the checkpoint's relative path." + ) + archive_path = build_dir / "roboflow_deploy.zip" + _write_zip( + archive_path, + [(weights, "weights.pt", True)] + [(path, name, name in required) for name, path in sorted(files.items())], + ) + return archive_path, "owlv2-custom", [] + + def _process_huggingface( model_type: str, model_path: Path, diff --git a/tests/util/test_model_processor.py b/tests/util/test_model_processor.py index da39b96e..ffbd97c7 100644 --- a/tests/util/test_model_processor.py +++ b/tests/util/test_model_processor.py @@ -728,6 +728,47 @@ def test_rfdetr_multiple_discovered_checkpoints_warns_which_used(self): bundle.cleanup() +class PackageOwlV2Test(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.source = Path(self.directory.name) + for name, contents in { + "config.json": '{"model_type": "owlv2"}', + "processor_config.json": "{}", + "tokenizer_config.json": "{}", + "tokenizer.json": "{}", + "class_names.txt": "dent\nscratch\n", + "owlv2.pth": "checkpoint", + }.items(): + (self.source / name).write_text(contents) + + def test_packages_raw_weights_without_loading_them(self): + with mock.patch.object(model_processor, "import_module", side_effect=AssertionError("No model load")): + bundle = package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + self.addCleanup(bundle.cleanup) + with zipfile.ZipFile(bundle.archive_path) as archive: + self.assertEqual(archive.read("weights.pt"), b"checkpoint") + self.assertEqual(archive.read("class_names.txt"), b"dent\nscratch\n") + self.assertIn("processor_config.json", archive.namelist()) + self.assertFalse((self.source / "weights.pt").exists()) + + def test_rejects_prompt_count_mismatch(self): + (self.source / "owlv2_prompts.json").write_text('["a dent"]') + with self.assertRaisesRegex(ModelPackagingError, "one nonblank text prompt"): + package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + + def test_rejects_duplicate_labels(self): + (self.source / "class_names.txt").write_text("dent\ndent\n") + with self.assertRaisesRegex(ModelPackagingError, "unique"): + package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + + def test_rejects_missing_processor(self): + (self.source / "processor_config.json").unlink() + with self.assertRaisesRegex(MissingFileError, "processor"): + package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + + class ProcessHuggingfaceTest(unittest.TestCase): """Packaging for HuggingFace-backed models (paligemma / florence-2).""" From 07677c9df65ff9cd1ad521829f976edf39007bfb Mon Sep 17 00:00:00 2001 From: Kai Christensen Date: Tue, 27 Oct 2026 12:00:03 -0600 Subject: [PATCH 2/7] Handle Windows newlines in the OWLv2 packaging test Co-authored-by: OpenAI Codex --- tests/util/test_model_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/util/test_model_processor.py b/tests/util/test_model_processor.py index ffbd97c7..5cf8e6f4 100644 --- a/tests/util/test_model_processor.py +++ b/tests/util/test_model_processor.py @@ -749,7 +749,7 @@ def test_packages_raw_weights_without_loading_them(self): self.addCleanup(bundle.cleanup) with zipfile.ZipFile(bundle.archive_path) as archive: self.assertEqual(archive.read("weights.pt"), b"checkpoint") - self.assertEqual(archive.read("class_names.txt"), b"dent\nscratch\n") + self.assertEqual(archive.read("class_names.txt").decode().splitlines(), ["dent", "scratch"]) self.assertIn("processor_config.json", archive.namelist()) self.assertFalse((self.source / "weights.pt").exists()) From da1db129c05f657fdf79dac47a42086ca9255558 Mon Sep 17 00:00:00 2001 From: Kai Christensen Date: Tue, 27 Oct 2026 12:01:31 -0600 Subject: [PATCH 3/7] Clarify OWLv2 label count and configuration requirements Co-authored-by: OpenAI Codex --- docs/owlv2-upload.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/owlv2-upload.md b/docs/owlv2-upload.md index 58782352..e8a9e3a6 100644 --- a/docs/owlv2-upload.md +++ b/docs/owlv2-upload.md @@ -22,9 +22,12 @@ Include either `processor_config.json` or `preprocessor_config.json` for image p If you omit `owlv2_prompts.json`, the model uses each label as its text prompt. For example, labels `dent` and `scratch` can use prompts `["a dent in a metal panel", "a scratch in a metal panel"]`. The class IDs follow the order in `class_names.txt`. +The weights do not impose a fixed class count. +Choose the labels that the model must detect, rather than a count inferred from the checkpoint. The checkpoint alone does not identify the original preprocessing or text prompts. Obtain those values from the training or inference code. +For an unchanged base model and processor, reuse the configuration and processor files from that original model. The exporter rejects partial checkpoints and adapter-only weights. The SDK packages files without loading the checkpoint. From 645da5885de267cc065845cfdd43c316f066cd7e Mon Sep 17 00:00:00 2001 From: Kai Christensen Date: Tue, 27 Oct 2026 15:43:59 -0600 Subject: [PATCH 4/7] Make the standard OWLv2 tokenizer optional on upload Co-authored-by: OpenAI Codex --- docs/owlv2-upload.md | 12 ++++++++++-- roboflow/util/model_processor.py | 17 +++++++++++----- tests/util/test_model_processor.py | 31 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/docs/owlv2-upload.md b/docs/owlv2-upload.md index e8a9e3a6..3a80f927 100644 --- a/docs/owlv2-upload.md +++ b/docs/owlv2-upload.md @@ -12,12 +12,20 @@ Save these files beside the checkpoint: | --- | --- | | `owlv2.pth` | The complete `Owlv2ForObjectDetection.state_dict()`, including the text encoder and detection heads | | `config.json` | The original model configuration, saved with `model.config.save_pretrained(folder)` | -| Processor and tokenizer files | The original processor, saved with `processor.save_pretrained(folder)` | +| Image processor files | The original image preprocessing configuration | +| Tokenizer files | Optional complete override of the standard OWLv2 tokenizer | | `class_names.txt` | Unique output labels, one per line | | `owlv2_prompts.json` | Optional JSON array of text prompts, in label order | -The processor files must include `tokenizer_config.json` and either `tokenizer.json` or both `vocab.json` and `merges.txt`. Include either `processor_config.json` or `preprocessor_config.json` for image processing. +If you omit all tokenizer files, the exporter uses a bundled copy of the standard OWLv2 tokenizer. +The copy comes from `google/owlv2-base-patch16-ensemble` at revision `cfd3195ba4ea9592eec887ded089f4c08eff231d`. +Export jobs use this copy offline and save it in the resulting model package. + +If you changed tokenization during training, include `tokenizer_config.json` and either `tokenizer.json` or both `vocab.json` and `merges.txt`. +Include any additional tokenizer files that your original processor saved, such as `added_tokens.json` and `special_tokens_map.json`. +Partial tokenizer overrides produce an error. The exporter does not mix uploaded tokenizer files with the default. +You can save the complete original processor with `processor.save_pretrained(folder)` to supply an override. If you omit `owlv2_prompts.json`, the model uses each label as its text prompt. For example, labels `dent` and `scratch` can use prompts `["a dent in a metal panel", "a scratch in a metal panel"]`. diff --git a/roboflow/util/model_processor.py b/roboflow/util/model_processor.py index a16bb605..f98b0d43 100644 --- a/roboflow/util/model_processor.py +++ b/roboflow/util/model_processor.py @@ -1131,13 +1131,16 @@ def _process_rfdetr( def _process_owlv2(model_path: Path, filename: str, build_dir: Path) -> tuple[Path, str, list[str]]: - required = {"config.json", "tokenizer_config.json", "class_names.txt"} - optional = { + required = {"config.json", "class_names.txt"} + tokenizer_files = { + "tokenizer_config.json", "tokenizer.json", "vocab.json", "merges.txt", "special_tokens_map.json", "added_tokens.json", + } + optional = tokenizer_files | { "preprocessor_config.json", "processor_config.json", "owlv2_prompts.json", @@ -1149,11 +1152,15 @@ def _process_owlv2(model_path: Path, filename: str, build_dir: Path) -> tuple[Pa f"Missing OWLv2 upload files: {', '.join(missing)}. " "Export the original model configuration and processor, and provide class_names.txt." ) - if not files["tokenizer.json"].is_file() and not all( - files[name].is_file() for name in ("vocab.json", "merges.txt") + if any(files[name].is_file() for name in tokenizer_files) and ( + not files["tokenizer_config.json"].is_file() + or not ( + files["tokenizer.json"].is_file() or all(files[name].is_file() for name in ("vocab.json", "merges.txt")) + ) ): raise MissingFileError( - "Provide tokenizer.json or both vocab.json and merges.txt from the original OWLv2 processor." + "Provide a complete tokenizer override: tokenizer_config.json and either tokenizer.json " + "or both vocab.json and merges.txt. Omit all tokenizer files to use the standard OWLv2 tokenizer." ) if not any(files[name].is_file() for name in ("preprocessor_config.json", "processor_config.json")): raise MissingFileError( diff --git a/tests/util/test_model_processor.py b/tests/util/test_model_processor.py index 5cf8e6f4..a223f38c 100644 --- a/tests/util/test_model_processor.py +++ b/tests/util/test_model_processor.py @@ -758,6 +758,37 @@ def test_rejects_prompt_count_mismatch(self): with self.assertRaisesRegex(ModelPackagingError, "one nonblank text prompt"): package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + def test_omits_tokenizer_for_server_default(self): + (self.source / "tokenizer_config.json").unlink() + (self.source / "tokenizer.json").unlink() + bundle = package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + self.addCleanup(bundle.cleanup) + with zipfile.ZipFile(bundle.archive_path) as archive: + self.assertNotIn("tokenizer_config.json", archive.namelist()) + self.assertNotIn("tokenizer.json", archive.namelist()) + self.assertIn("processor_config.json", archive.namelist()) + + def test_rejects_partial_tokenizer_overrides(self): + (self.source / "tokenizer_config.json").unlink() + (self.source / "tokenizer.json").unlink() + for name in ("tokenizer_config.json", "tokenizer.json", "special_tokens_map.json", "vocab.json"): + with self.subTest(name=name): + (self.source / name).write_text("{}") + with self.assertRaisesRegex(MissingFileError, "complete tokenizer override"): + package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + (self.source / name).unlink() + + def test_packages_legacy_tokenizer_override(self): + (self.source / "tokenizer.json").unlink() + (self.source / "vocab.json").write_text("{}") + (self.source / "merges.txt").write_text("#version: 0.2\n") + bundle = package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + self.addCleanup(bundle.cleanup) + with zipfile.ZipFile(bundle.archive_path) as archive: + self.assertIn("tokenizer_config.json", archive.namelist()) + self.assertIn("vocab.json", archive.namelist()) + self.assertIn("merges.txt", archive.namelist()) + def test_rejects_duplicate_labels(self): (self.source / "class_names.txt").write_text("dent\ndent\n") with self.assertRaisesRegex(ModelPackagingError, "unique"): From 8ce83c8637a027c2f40af609e5221f9410ebec25 Mon Sep 17 00:00:00 2001 From: Kai Christensen Date: Tue, 27 Oct 2026 16:09:57 -0600 Subject: [PATCH 5/7] Default OWLv2 uploads to weights and class names Co-authored-by: OpenAI Codex --- docs/owlv2-upload.md | 75 ++++++++++++++++-------------- roboflow/util/model_processor.py | 14 ++---- tests/util/test_model_processor.py | 20 ++++++-- 3 files changed, 62 insertions(+), 47 deletions(-) diff --git a/docs/owlv2-upload.md b/docs/owlv2-upload.md index 3a80f927..ac444c20 100644 --- a/docs/owlv2-upload.md +++ b/docs/owlv2-upload.md @@ -1,43 +1,23 @@ # Upload a custom OWLv2 model -Use `model_type="owlv2-custom"` to upload a complete OWLv2 parameter dictionary. -This model uses saved text prompts and returns object detections with fixed labels. -It works through the custom-model Auto Label path and the Object Detection Model Workflow block. +Use `model_type="owlv2-custom"` to upload fine-tuned OWLv2 weights for Auto Label and Workflows. +For a standard OWLv2 model, the upload folder needs only two files: -## Prepare the upload folder +```text +weights-upload/ + owlv2.pth + class_names.txt +``` -Save these files beside the checkpoint: +`owlv2.pth` must contain the complete `Owlv2ForObjectDetection.state_dict()`, including the text encoder and detection heads. +A training checkpoint that wraps the weights under another key is not accepted. +`class_names.txt` contains unique, nonblank output labels, one per line. Class IDs follow this order. +The labels also become the text prompts. The weights do not impose a fixed class count. -| File | Content | -| --- | --- | -| `owlv2.pth` | The complete `Owlv2ForObjectDetection.state_dict()`, including the text encoder and detection heads | -| `config.json` | The original model configuration, saved with `model.config.save_pretrained(folder)` | -| Image processor files | The original image preprocessing configuration | -| Tokenizer files | Optional complete override of the standard OWLv2 tokenizer | -| `class_names.txt` | Unique output labels, one per line | -| `owlv2_prompts.json` | Optional JSON array of text prompts, in label order | - -Include either `processor_config.json` or `preprocessor_config.json` for image processing. -If you omit all tokenizer files, the exporter uses a bundled copy of the standard OWLv2 tokenizer. -The copy comes from `google/owlv2-base-patch16-ensemble` at revision `cfd3195ba4ea9592eec887ded089f4c08eff231d`. -Export jobs use this copy offline and save it in the resulting model package. - -If you changed tokenization during training, include `tokenizer_config.json` and either `tokenizer.json` or both `vocab.json` and `merges.txt`. -Include any additional tokenizer files that your original processor saved, such as `added_tokens.json` and `special_tokens_map.json`. -Partial tokenizer overrides produce an error. The exporter does not mix uploaded tokenizer files with the default. -You can save the complete original processor with `processor.save_pretrained(folder)` to supply an override. - -If you omit `owlv2_prompts.json`, the model uses each label as its text prompt. -For example, labels `dent` and `scratch` can use prompts `["a dent in a metal panel", "a scratch in a metal panel"]`. -The class IDs follow the order in `class_names.txt`. -The weights do not impose a fixed class count. -Choose the labels that the model must detect, rather than a count inferred from the checkpoint. - -The checkpoint alone does not identify the original preprocessing or text prompts. -Obtain those values from the training or inference code. -For an unchanged base model and processor, reuse the configuration and processor files from that original model. -The exporter rejects partial checkpoints and adapter-only weights. -The SDK packages files without loading the checkpoint. +The exporter assumes the standard `google/owlv2-base-patch16-ensemble` model configuration, image preprocessing, and tokenizer. +It bundles these defaults at revision `cfd3195ba4ea9592eec887ded089f4c08eff231d` and uses them offline. +Fine-tuning the weights does not require additional files if those components stay unchanged. +The exporter saves the resolved configuration and processor in the inference package. ## Upload @@ -62,4 +42,29 @@ After upload processing finishes, select the uploaded model under custom models In Workflows, select its model ID in an Object Detection Model block. The saved prompts stay fixed for each model package. +## Custom model or preprocessing + +If training changed a component, place its override files beside the weights. +Each supplied component takes precedence over its default. + +| Optional files | When to supply them | +| --- | --- | +| `config.json` | A different model architecture, saved with `model.config.save_pretrained(folder)` | +| `processor_config.json` or `preprocessor_config.json` | Different image preprocessing, such as resizing or normalization | +| Tokenizer files | Different tokenization, saved with the original tokenizer or processor | +| `owlv2_prompts.json` | Text prompts that differ from the output labels | + +A tokenizer override needs `tokenizer_config.json` and either `tokenizer.json` or both `vocab.json` and `merges.txt`. +Include additional tokenizer files that the original processor saved, such as `added_tokens.json` and `special_tokens_map.json`. +Partial tokenizer overrides produce an error. They are never mixed with the default tokenizer. +You can save a complete processor override with `processor.save_pretrained(folder)`. + +`owlv2_prompts.json` is a JSON array with one nonblank prompt per label, in label order. +For example, labels `dent` and `scratch` can use `["a dent in a metal panel", "a scratch in a metal panel"]`. + +The exporter strictly matches the weights to the selected model configuration and rejects partial or adapter-only checkpoints. +If the weights do not match the default architecture, supply the original `config.json`. +Weight shapes cannot identify custom resizing or normalization. Omitted image preprocessing files mean standard preprocessing. +The SDK packages files without loading the checkpoint. + This feature requires the corresponding platform, rflm exporter, and inference runtime releases. diff --git a/roboflow/util/model_processor.py b/roboflow/util/model_processor.py index f98b0d43..39d5d344 100644 --- a/roboflow/util/model_processor.py +++ b/roboflow/util/model_processor.py @@ -1131,7 +1131,7 @@ def _process_rfdetr( def _process_owlv2(model_path: Path, filename: str, build_dir: Path) -> tuple[Path, str, list[str]]: - required = {"config.json", "class_names.txt"} + required = {"class_names.txt"} tokenizer_files = { "tokenizer_config.json", "tokenizer.json", @@ -1141,6 +1141,7 @@ def _process_owlv2(model_path: Path, filename: str, build_dir: Path) -> tuple[Pa "added_tokens.json", } optional = tokenizer_files | { + "config.json", "preprocessor_config.json", "processor_config.json", "owlv2_prompts.json", @@ -1149,8 +1150,7 @@ def _process_owlv2(model_path: Path, filename: str, build_dir: Path) -> tuple[Pa missing = sorted(name for name in required if not files[name].is_file()) if missing: raise MissingFileError( - f"Missing OWLv2 upload files: {', '.join(missing)}. " - "Export the original model configuration and processor, and provide class_names.txt." + f"Missing OWLv2 upload files: {', '.join(missing)}. Provide class_names.txt with one output label per line." ) if any(files[name].is_file() for name in tokenizer_files) and ( not files["tokenizer_config.json"].is_file() @@ -1162,12 +1162,8 @@ def _process_owlv2(model_path: Path, filename: str, build_dir: Path) -> tuple[Pa "Provide a complete tokenizer override: tokenizer_config.json and either tokenizer.json " "or both vocab.json and merges.txt. Omit all tokenizer files to use the standard OWLv2 tokenizer." ) - if not any(files[name].is_file() for name in ("preprocessor_config.json", "processor_config.json")): - raise MissingFileError( - "Provide preprocessor_config.json or processor_config.json from the original OWLv2 processor." - ) try: - config = json.loads(files["config.json"].read_text()) + config = json.loads(files["config.json"].read_text()) if files["config.json"].is_file() else None class_names = files["class_names.txt"].read_text().splitlines() prompts = ( json.loads(files["owlv2_prompts.json"].read_text()) @@ -1176,7 +1172,7 @@ def _process_owlv2(model_path: Path, filename: str, build_dir: Path) -> tuple[Pa ) except (ValueError, UnicodeError) as error: raise ModelPackagingError(f"Invalid OWLv2 metadata: {error}") from error - if not isinstance(config, dict) or config.get("model_type") != "owlv2": + if files["config.json"].is_file() and (not isinstance(config, dict) or config.get("model_type") != "owlv2"): raise ModelPackagingError("config.json must describe an OWLv2 model (model_type='owlv2').") if ( not class_names diff --git a/tests/util/test_model_processor.py b/tests/util/test_model_processor.py index a223f38c..83b0054a 100644 --- a/tests/util/test_model_processor.py +++ b/tests/util/test_model_processor.py @@ -794,9 +794,23 @@ def test_rejects_duplicate_labels(self): with self.assertRaisesRegex(ModelPackagingError, "unique"): package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") - def test_rejects_missing_processor(self): - (self.source / "processor_config.json").unlink() - with self.assertRaisesRegex(MissingFileError, "processor"): + def test_packages_only_weights_and_class_names(self): + for path in self.source.iterdir(): + if path.name not in {"owlv2.pth", "class_names.txt"}: + path.unlink() + bundle = package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + self.addCleanup(bundle.cleanup) + with zipfile.ZipFile(bundle.archive_path) as archive: + self.assertEqual(set(archive.namelist()), {"weights.pt", "class_names.txt"}) + + def test_rejects_invalid_model_configuration_override(self): + (self.source / "config.json").write_text('{"model_type": "owlvit"}') + with self.assertRaisesRegex(ModelPackagingError, "config.json must describe an OWLv2 model"): + package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + + def test_rejects_missing_class_names(self): + (self.source / "class_names.txt").unlink() + with self.assertRaisesRegex(MissingFileError, "class_names.txt"): package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") From 09d5cf88366ef801c0c9f2df22a49b5b0d1ca04a Mon Sep 17 00:00:00 2001 From: Kai Christensen Date: Wed, 28 Oct 2026 19:39:51 -0600 Subject: [PATCH 6/7] Reuse the owlv2 model type for checkpoint uploads Co-authored-by: OpenAI Codex --- docs/owlv2-upload.md | 5 +++-- roboflow/util/model_processor.py | 10 +++++----- tests/util/test_model_processor.py | 18 +++++++++--------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/owlv2-upload.md b/docs/owlv2-upload.md index ac444c20..75ef28a2 100644 --- a/docs/owlv2-upload.md +++ b/docs/owlv2-upload.md @@ -1,6 +1,6 @@ # Upload a custom OWLv2 model -Use `model_type="owlv2-custom"` to upload fine-tuned OWLv2 weights for Auto Label and Workflows. +Use `model_type="owlv2"` to upload fine-tuned OWLv2 weights for Auto Label and Workflows. For a standard OWLv2 model, the upload folder needs only two files: ```text @@ -30,7 +30,7 @@ from roboflow import Roboflow workspace = Roboflow().workspace("your-workspace") workspace.deploy_model( - model_type="owlv2-custom", + model_type="owlv2", model_path="/absolute/path/to/weights-upload", filename="owlv2.pth", project_ids=["your-project"], @@ -41,6 +41,7 @@ workspace.deploy_model( After upload processing finishes, select the uploaded model under custom models in Auto Label. In Workflows, select its model ID in an Object Detection Model block. The saved prompts stay fixed for each model package. +Inference reuses the existing OWLv2 implementation. Roboflow Instant embedding uploads keep their separate format and route. ## Custom model or preprocessing diff --git a/roboflow/util/model_processor.py b/roboflow/util/model_processor.py index 39d5d344..83ffadbb 100644 --- a/roboflow/util/model_processor.py +++ b/roboflow/util/model_processor.py @@ -68,7 +68,7 @@ "paligemma2", "florence-2", "rfdetr", - "owlv2-custom", + "owlv2", ) SUPPORTED_HUGGINGFACE_TYPES = ( @@ -463,10 +463,10 @@ def _process_model( f"It must start with a supported family: {', '.join(SUPPORTED_MODELS)}." ) - if model_type == "owlv2-custom": + if model_type == "owlv2": return _process_owlv2(model_path, filename, build_dir) - elif model_type.startswith("owlv2-custom"): - raise UnsupportedModelError("Use model_type='owlv2-custom' for OWLv2 checkpoints.") + elif model_type.startswith("owlv2"): + raise UnsupportedModelError("Use model_type='owlv2' for OWLv2 checkpoints.") elif model_type.startswith(("paligemma", "paligemma2", "florence-2")): return _process_huggingface(model_type, model_path, build_dir) if model_type.startswith("yolonas"): @@ -1200,7 +1200,7 @@ def _process_owlv2(model_path: Path, filename: str, build_dir: Path) -> tuple[Pa archive_path, [(weights, "weights.pt", True)] + [(path, name, name in required) for name, path in sorted(files.items())], ) - return archive_path, "owlv2-custom", [] + return archive_path, "owlv2", [] def _process_huggingface( diff --git a/tests/util/test_model_processor.py b/tests/util/test_model_processor.py index 83b0054a..e2b39ded 100644 --- a/tests/util/test_model_processor.py +++ b/tests/util/test_model_processor.py @@ -745,7 +745,7 @@ def setUp(self): def test_packages_raw_weights_without_loading_them(self): with mock.patch.object(model_processor, "import_module", side_effect=AssertionError("No model load")): - bundle = package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + bundle = package_custom_weights("owlv2", str(self.source), "owlv2.pth") self.addCleanup(bundle.cleanup) with zipfile.ZipFile(bundle.archive_path) as archive: self.assertEqual(archive.read("weights.pt"), b"checkpoint") @@ -756,12 +756,12 @@ def test_packages_raw_weights_without_loading_them(self): def test_rejects_prompt_count_mismatch(self): (self.source / "owlv2_prompts.json").write_text('["a dent"]') with self.assertRaisesRegex(ModelPackagingError, "one nonblank text prompt"): - package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + package_custom_weights("owlv2", str(self.source), "owlv2.pth") def test_omits_tokenizer_for_server_default(self): (self.source / "tokenizer_config.json").unlink() (self.source / "tokenizer.json").unlink() - bundle = package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + bundle = package_custom_weights("owlv2", str(self.source), "owlv2.pth") self.addCleanup(bundle.cleanup) with zipfile.ZipFile(bundle.archive_path) as archive: self.assertNotIn("tokenizer_config.json", archive.namelist()) @@ -775,14 +775,14 @@ def test_rejects_partial_tokenizer_overrides(self): with self.subTest(name=name): (self.source / name).write_text("{}") with self.assertRaisesRegex(MissingFileError, "complete tokenizer override"): - package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + package_custom_weights("owlv2", str(self.source), "owlv2.pth") (self.source / name).unlink() def test_packages_legacy_tokenizer_override(self): (self.source / "tokenizer.json").unlink() (self.source / "vocab.json").write_text("{}") (self.source / "merges.txt").write_text("#version: 0.2\n") - bundle = package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + bundle = package_custom_weights("owlv2", str(self.source), "owlv2.pth") self.addCleanup(bundle.cleanup) with zipfile.ZipFile(bundle.archive_path) as archive: self.assertIn("tokenizer_config.json", archive.namelist()) @@ -792,13 +792,13 @@ def test_packages_legacy_tokenizer_override(self): def test_rejects_duplicate_labels(self): (self.source / "class_names.txt").write_text("dent\ndent\n") with self.assertRaisesRegex(ModelPackagingError, "unique"): - package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + package_custom_weights("owlv2", str(self.source), "owlv2.pth") def test_packages_only_weights_and_class_names(self): for path in self.source.iterdir(): if path.name not in {"owlv2.pth", "class_names.txt"}: path.unlink() - bundle = package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + bundle = package_custom_weights("owlv2", str(self.source), "owlv2.pth") self.addCleanup(bundle.cleanup) with zipfile.ZipFile(bundle.archive_path) as archive: self.assertEqual(set(archive.namelist()), {"weights.pt", "class_names.txt"}) @@ -806,12 +806,12 @@ def test_packages_only_weights_and_class_names(self): def test_rejects_invalid_model_configuration_override(self): (self.source / "config.json").write_text('{"model_type": "owlvit"}') with self.assertRaisesRegex(ModelPackagingError, "config.json must describe an OWLv2 model"): - package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + package_custom_weights("owlv2", str(self.source), "owlv2.pth") def test_rejects_missing_class_names(self): (self.source / "class_names.txt").unlink() with self.assertRaisesRegex(MissingFileError, "class_names.txt"): - package_custom_weights("owlv2-custom", str(self.source), "owlv2.pth") + package_custom_weights("owlv2", str(self.source), "owlv2.pth") class ProcessHuggingfaceTest(unittest.TestCase): From 733d3d37d899772bac44688a96efca42471952cd Mon Sep 17 00:00:00 2001 From: Kai Christensen Date: Wed, 28 Oct 2026 21:18:10 -0600 Subject: [PATCH 7/7] Remove internal release coordination from OWLv2 upload docs Co-authored-by: OpenAI Codex --- docs/owlv2-upload.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/owlv2-upload.md b/docs/owlv2-upload.md index 75ef28a2..d51988a6 100644 --- a/docs/owlv2-upload.md +++ b/docs/owlv2-upload.md @@ -67,5 +67,3 @@ The exporter strictly matches the weights to the selected model configuration an If the weights do not match the default architecture, supply the original `config.json`. Weight shapes cannot identify custom resizing or normalization. Omitted image preprocessing files mean standard preprocessing. The SDK packages files without loading the checkpoint. - -This feature requires the corresponding platform, rflm exporter, and inference runtime releases.