feat: add sort-by-confidence option to WD14 captioning#3506
Conversation
Removed all usage of the `easygui` library.
- Replaced `easygui.msgbox` calls with Gradio notifications (`gr.Info`, `gr.Warning`, `gr.Error`) to display messages and errors directly in the UI.
- Replaced `easygui.ynbox` and `easygui.boolbox` confirmation dialogs with a default action of proceeding with the operation, accompanied by a notification. This affects:
- Model saving: Will now overwrite existing models by default, notifying the user.
- Dataset balancing: Enabling 'insecure folder renaming' will proceed without a second confirmation, with a warning displayed.
- Manual captioning: Importing tags will overwrite existing quick tags by default, with a notification.
- Removed `easygui` from all Python import statements.
- Removed `easygui` from `requirements.txt` and `pyproject.toml`.
Note: The `uv.lock` file has not been modified as part of this commit. It should be regenerated using the `uv` tool to reflect the dependency changes in `pyproject.toml` and `requirements.txt`.
Centralize version information by reading it from the `[project.version]` field in `pyproject.toml` instead of the separate `.release` file. Changes: - Modified `kohya_gui.py` to use the `toml` library to parse `pyproject.toml` and retrieve the version string. - Added a "v" prefix to the version for display consistency. - Included a fallback to "vUNKNOWN" if `pyproject.toml` is missing or the version cannot be read. - Deleted the now-unused `.release` file. Note: Full runtime testing was prevented by an out-of-disk-space error in the test environment when installing dependencies. The core code change for version sourcing has been implemented as requested.
Refactor: Read version from pyproject.toml
Refactor: Remove easygui and replace dialogs with notifications
Expand all accordions option
* Aligned config.toml with gradio elements * Align Gradio UI elements with config.toml Ensured that all relevant Gradio UI elements in Dreambooth, Finetune, LoRA, and Textual Inversion GUIs, along with their supporting classes, load their default values from `config.toml`. - Added missing keys to `config example.toml` for newly configurable elements. - Modified Python files to use `config.get()` for these elements, preserving original default behaviors if keys are not in `config.toml`. - Corrected minor inconsistencies in existing `config.get()` usage. * Update alignment --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
- Added and updated comments for better understanding of parameters. - Improved formatting and alignment for readability. - Corrected misleading comments regarding `cpu_offload_checkpointing` and the purpose of the final `[dataset_preparation]` section. - Ensured all key names and TOML structure used by the GUI remain unchanged. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.32.0 to 1.34.0. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](crate-ci/typos@v1.32.0...v1.34.0) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-version: 1.34.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: bmaltais <bernard@ducourier.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
The `libsycl.so.7` error was caused by an incompatibility with the installed oneAPI version. This commit updates the following packages to their latest versions to resolve the issue: - intel-extension-for-tensorflow[xpu] - mkl - mkl-dpcpp - oneccl-devel - impi-devel Co-authored-by: bmaltais <bernard@ducourier.com> Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* docs: Add documentation for allowed_paths This change adds documentation for the `allowed_paths` config option to the `README.md` file. * docs: Add server section to config example This change adds the `[server]` section to the `config example.toml` file, including the `allowed_paths` option. --------- Co-authored-by: bmaltais <bernard@ducourier.com> Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: bmaltais <bernard@ducourier.com>
* Fix(tensorboard): Add cross-platform AVX check I replaced the Linux-specific AVX check with a cross-platform solution using the `py-cpuinfo` library. This ensures that the TensorBoard feature is only enabled on systems with AVX support, regardless of the operating system. The new implementation provides a more robust solution that prevents crashes on incompatible hardware while maintaining a consistent user experience across different platforms. * Update pyproject --------- Co-authored-by: bmaltais <bernard@ducourier.com> Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* feat: Apply Gradio GUI improvements to manual_caption_gui.py This commit applies the same Gradio GUI improvements that were done in `kintext_manual_caption_gui.py` to `manual_caption_gui.py`. The changes include: - Better user feedback for status updates. - Safer return values from functions to avoid clearing user input on errors. - A more organized layout with better-labeled components. - Clearer pagination controls. * refactor: Align manual_caption_gui.py with new standards This commit refactors the `manual_caption_gui.py` file to align with the new standards set by the `kontext_manual_caption_gui.py` file. The changes include: - Performance and Efficiency: Use of `os.path.basename`, `os.scandir`, and `gr.State` to improve performance. - Code Quality and Readability: Case-insensitive tag matching, sorted tags, and improved variable names. - UI and User Experience: Refined UI layout, consolidated pagination logic, and improved error handling. * fix: Correct pagination display issue This commit fixes a bug in the pagination logic that was causing the incorrect page count to be displayed. The `page.change` event handler has been updated to correctly update the `page_count` component when the page number or the total number of pages changes. * fix: Correct pagination and resolve ValueError This commit fixes a bug in the pagination logic that was causing the incorrect page count to be displayed. The `page.change` event handler has been updated to correctly update the `page_count` component when the page number or the total number of pages changes. This commit also resolves a `ValueError` that was occurring in the `render_pagination_with_logic` function. The `page.change` event handler has been updated to return a tuple of two `gr.Text` components, which resolves the error. * fix: Correct pagination on initial load This commit fixes a bug where the pagination would not display the correct number of pages on the initial load. A `max_page.change` event handler has been added to both `manual_caption_gui.py` and `kontext_manual_caption_gui.py` to ensure the page count is updated correctly. * Update gui layout --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* feat: Suggest target folder in kontext manual caption GUI * feat: Suggest target folder in kontext manual caption GUI - The suggested path for the target folder is now the full path. - The target folder is only updated if it's empty. * feat: Suggest target folder in kontext manual caption GUI - The suggested path for the target folder is now the full path. - The target folder is only updated if it's empty. * feat: Suggest target folder in kontext manual caption GUI - The suggested path for the target folder is now the full path. - The target folder is only updated if it's empty. - The logic for deriving the target folder has been updated. * feat: Suggest target folder in kontext manual caption GUI - The suggested path for the target folder is now the full path. - The target folder is only updated if it's empty. - The logic for deriving the target folder has been updated. - Tags are now automatically imported from captions when images are loaded. * Sort tags --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* feat: Add Flux manual captioning GUI * This looks good. I have corrected the output values for `update_images` in the Flux captioning GUI. * feat: Add Kontext manual captioning GUI * Swap control and target position --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* fix pagitation * fix pagitation, fix page numbering
* feat: Update gui-uv.sh for Colab parity and add local scripts This commit introduces changes to align the local setup via `gui-uv.sh` more closely with the procedures in the reference Colab notebook, particularly concerning `bitsandbytes` compilation. Changes: - Added `kohya_ss_colab.ipynb` to the repository for reference. - Created `local_train.py`, a Python script adapted from the Colab notebook, providing an alternative setup and launch method. - Modified `gui-uv.sh` to include a documented, optional section for users to build `bitsandbytes` from source if the `uv`-installed version is insufficient. This enhances flexibility and aids in troubleshooting CUDA/GPU-specific issues. The primary goal is to make the local experience with `gui-uv.sh` more robust and provide users with options similar to the Colab environment's setup for critical dependencies like `bitsandbytes`. * Update collab * Update colab script --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit adds the ability to delete a pair of images and their corresponding caption file from the kontext manual captioning tool. A "Delete" button has been added to the UI for each image pair. When clicked, this button triggers the deletion of the control image, the target image, and the caption file. The UI is then refreshed to reflect the changes. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* upgraded the versions of some dependencies * added onnxruntime to requirements_linux_ipex.txt * upgraded the versions of torch and torchvision * downgraded the torch version to match the official image
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.32.0 to 1.34.0. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](crate-ci/typos@v1.32.0...v1.34.0) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-version: 1.34.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: bmaltais <bernard@ducourier.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
Adds a "Sort tags by confidence" checkbox to the WD14 Captioning tab. When enabled, the --sort_tags_by_confidence flag is passed to the tagger script, which sorts general and character tags by their probability score (highest first) before writing the caption file. This matches the tag ordering produced by the WaifuDiffusion online tagger, where high-confidence subjects (2girls, 1boy, etc.) naturally appear first. Default is unchecked, preserving existing CSV-index ordering behavior. Closes #3491
There was a problem hiding this comment.
Pull request overview
This PR adds a WD14 Captioning GUI option to sort output tags by model confidence (to match WaifuDiffusion’s online tagger ordering), and also includes several additional changes across captioning utilities, server launch configuration, TensorBoard detection, docs, and dependency/config updates.
Changes:
- Add a “Sort tags by confidence” checkbox to the WD14 captioning tab and forward it as a CLI flag.
- Add Gradio server
allowed_pathssupport viaconfig.toml, and document it. - Refactor manual captioning UI and add a new “Kontext Manual Captioning” tab, plus related dependency/tests/scripts updates.
Reviewed changes
Copilot reviewed 19 out of 21 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
uv.lock |
Adds py-cpuinfo to the locked dependency set. |
requirements.txt |
Adds py-cpuinfo dependency. |
requirements_macos_arm64.txt |
Adjusts pip options / removes an entry to align macOS ARM requirements. |
requirements_linux_ipex.txt |
Updates IPEX/XPU package pins and indexes. |
README.md |
Updates docs links/TOC and documents allowed_paths under “Server Options”. |
pyproject.toml |
Adds py-cpuinfo dependency. |
local_train.py |
Adds a local training/setup helper script. |
kohya_ss_colab.ipynb |
Adds a Colab notebook for launching via gui-uv.sh. |
kohya_gui/wd14_caption_gui.py |
Adds the “sort by confidence” checkbox and forwards --sort_tags_by_confidence. |
kohya_gui/utilities.py |
Registers the new “Kontext Manual Captioning” tab in Utilities → Captioning. |
kohya_gui/manual_caption_gui.py |
Refactors manual captioning UI/state handling and import logic. |
kohya_gui/kontext_manual_caption_gui.py |
Adds a new manual captioning workflow for paired control/target images. |
kohya_gui/class_tensorboard.py |
Changes TensorBoard visibility detection to depend on tensorboard binary + AVX CPU support. |
kohya_gui/class_gui_config.py |
Adds allowed_paths config extraction and stores it on the config object. |
kohya_gui.py |
Passes allowed_paths into gradio.Blocks.launch(). |
gui-uv.sh |
Adds commented guidance for optional bitsandbytes source builds. |
config example.toml |
Documents new [server].allowed_paths configuration key. |
tests/test_tensorboard_visibility.py |
Adds unit tests for TensorBoard visibility/AVX detection logic. |
test/test_allowed_paths.py |
Adds an integration-style test attempting to validate allowed_paths. |
.github/workflows/typos.yaml |
Bumps crate-ci/typos GitHub Action version. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if use_rating_tags: | ||
| run_cmd.append("--use_rating_tags") | ||
| if use_rating_tags_as_last_tag: | ||
| run_cmd.append("--use_rating_tags_as_last_tag") | ||
| if sort_tags_by_confidence: | ||
| run_cmd.append("--sort_tags_by_confidence") | ||
|
|
||
| # Add the directory containing the training data | ||
| run_cmd.append(rf"{train_data_dir}") |
| @@ -100,7 +105,7 @@ def update_image_tags( | |||
| ] | |||
| caption = ", ".join(output_tags) | |||
| empty_row = gr.Row(visible=False) | ||
| return [empty_row] * (IMAGES_TO_SHOW * 4 + 2) | ||
|
|
| selected_tags_set = set(selected_tags) | ||
|
|
||
| output_tags = [t for t in quick_tags if t in selected_tags_set] + [ | ||
| t for t in selected_tags if t not in quick_tags_set |
| # Return empty updates if state is not ready | ||
| empty_row = gr.Row(visible=False) | ||
| return [empty_row] * (IMAGES_TO_SHOW * 5 + 2) | ||
|
|
| # Since we are modifying an existing file, we need to reload it | ||
| import kohya_gui.class_tensorboard | ||
| importlib.reload(kohya_gui.class_tensorboard) | ||
|
|
||
| class TestTensorboardVisibility(unittest.TestCase): | ||
|
|
||
| @patch('shutil.which', return_value='/usr/bin/tensorboard') | ||
| @patch('cpuinfo.get_cpu_info', return_value={'flags': ['avx']}) | ||
| def test_tensorboard_visibility_when_tensorboard_and_avx_are_present(self, mock_cpuinfo, mock_which): | ||
| importlib.reload(kohya_gui.class_tensorboard) | ||
| self.assertTrue(kohya_gui.class_tensorboard.visibility) | ||
|
|
||
| @patch('shutil.which', return_value=None) | ||
| @patch('cpuinfo.get_cpu_info', return_value={'flags': ['avx']}) | ||
| def test_tensorboard_visibility_when_tensorboard_is_absent(self, mock_cpuinfo, mock_which): | ||
| importlib.reload(kohya_gui.class_tensorboard) | ||
| self.assertFalse(kohya_gui.class_tensorboard.visibility) | ||
|
|
||
| @patch('shutil.which', return_value='/usr/bin/tensorboard') | ||
| @patch('cpuinfo.get_cpu_info', return_value={'flags': ['sse']}) | ||
| def test_tensorboard_visibility_when_avx_is_absent(self, mock_cpuinfo, mock_which): | ||
| importlib.reload(kohya_gui.class_tensorboard) | ||
| self.assertFalse(kohya_gui.class_tensorboard.visibility) | ||
|
|
||
| @patch('cpuinfo.get_cpu_info', side_effect=Exception) | ||
| def test_check_avx_support_exception(self, mock_cpuinfo): | ||
| self.assertFalse(kohya_gui.class_tensorboard.check_avx_support()) |
| def test_allowed_paths(self): | ||
| print("Running test_allowed_paths...") | ||
| # Run the gui with the new config and check if it can access the dummy file | ||
| process = subprocess.Popen( | ||
| [ | ||
| "python", | ||
| "kohya_gui.py", | ||
| "--config", | ||
| str(self.config_file), | ||
| "--headless", | ||
| ], | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.PIPE, | ||
| ) | ||
|
|
||
| print("Process started.") | ||
| # Give the server some time to start | ||
| try: | ||
| stdout, stderr = process.communicate(timeout=10) | ||
| except subprocess.TimeoutExpired: | ||
| process.kill() | ||
| stdout, stderr = process.communicate() | ||
|
|
||
| print(f"Stdout: {stdout.decode()}") | ||
| print(f"Stderr: {stderr.decode()}") | ||
| # Check if there are any errors in the stderr | ||
| self.assertNotIn("InvalidPathError", stderr.decode()) | ||
| print("Test complete.") |
| "id": "view-in-github" | ||
| }, | ||
| "source": [ | ||
| "[](https://colab.research.google.com/github/bmaltais/kohya_ss/blob/feature/update-gui-uv-for-colab-parity/kohya_ss_colab.ipynb)" |
| def run_command(command): | ||
| """Runs a shell command and prints its output.""" | ||
| print(f"Running command: {command}") | ||
| process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) | ||
| for line in process.stdout: | ||
| print(line, end="") | ||
| process.wait() | ||
| if process.returncode != 0: | ||
| raise Exception(f"Command failed with return code {process.returncode}: {command}") |
| # Gradio UI | ||
| def gradio_manual_caption_gui_tab(headless=False, default_images_dir=None): | ||
| from .common_gui import create_refresh_button | ||
|
|
||
| default_images_dir = ( | ||
| default_images_dir | ||
| if default_images_dir is not None | ||
| else os.path.join(scriptdir, "data") | ||
| ) | ||
| current_images_dir = default_images_dir | ||
| default_images_dir = default_images_dir or os.path.join(scriptdir, "data") | ||
|
|
||
| def update_dir_list(path): | ||
| return gr.Dropdown(choices=[""] + list(list_dirs(path))) | ||
|
|
||
| # Function to list directories | ||
| def list_images_dirs(path): | ||
| # Allows list_images_dirs to modify current_images_dir outside of this function | ||
| nonlocal current_images_dir | ||
| current_images_dir = path | ||
| return list(list_dirs(path)) | ||
| def render_pagination_with_logic(page, max_page): | ||
| with gr.Row(visible=False) as pagination_row: | ||
| gr.Button("◀ Prev").click(paginate, inputs=[page, max_page, gr.Number(-1, visible=False)], outputs=[page]) | ||
| page_count = gr.Text("Page 1 / 1", show_label=False, interactive=False, text_align="center") | ||
| page_goto_text = gr.Textbox(show_label=False, placeholder="Go to page...", container=False, scale=1) | ||
| gr.Button("Next ▶").click(paginate, inputs=[page, max_page, gr.Number(1, visible=False)], outputs=[page]) | ||
| page_goto_text.submit(paginate_go, inputs=[page_goto_text, max_page], outputs=[page]) | ||
| return pagination_row, page_count | ||
|
|
||
| with gr.Tab("Manual Captioning"): | ||
| gr.Markdown("This utility allows quick captioning and tagging of images.") | ||
| page = gr.Number(value=-1, visible=False) | ||
|
|
||
| image_files_state = gr.State([]) | ||
|
|
||
| info_box = gr.Markdown(visible=False) | ||
| page = gr.State(value=1) | ||
| max_page = gr.Number(value=1, visible=False) | ||
| loaded_images_dir = gr.Text(visible=False) | ||
| with gr.Group(), gr.Row(): | ||
| images_dir = gr.Dropdown( | ||
| label="Image folder to caption (containing the images to caption)", | ||
| choices=[""] + list_images_dirs(default_images_dir), | ||
| value="", | ||
| interactive=True, | ||
| allow_custom_value=True, | ||
| ) | ||
| create_refresh_button( | ||
| images_dir, | ||
| lambda: None, | ||
| lambda: {"choices": list_images_dirs(current_images_dir)}, | ||
| "open_folder_small", | ||
| ) | ||
| folder_button = gr.Button( | ||
| "📂", | ||
| elem_id="open_folder_small", | ||
| elem_classes=["tool"], | ||
| visible=(not headless), | ||
| ) | ||
| folder_button.click( | ||
| get_folder_path, | ||
| outputs=images_dir, | ||
| show_progress=False, | ||
| ) | ||
| load_images_button = gr.Button("Load", elem_id="open_folder") | ||
| caption_ext = gr.Dropdown( | ||
| label="Caption file extension", | ||
| choices=[".cap", ".caption", ".txt"], | ||
| value=".txt", | ||
| interactive=True, | ||
| allow_custom_value=True, | ||
| ) | ||
| auto_save = gr.Checkbox( | ||
| label="Autosave", info="Options", value=True, interactive=True | ||
| ) | ||
|
|
||
| images_dir.change( | ||
| fn=lambda path: gr.Dropdown(choices=[""] + list_images_dirs(path)), | ||
| inputs=images_dir, | ||
| outputs=images_dir, | ||
| show_progress=False, | ||
| ) | ||
|
|
||
| # Caption Section | ||
| with gr.Group(), gr.Row(): | ||
| quick_tags_text = gr.Textbox( | ||
| label="Quick Tags", | ||
| placeholder="Comma separated list of tags", | ||
| interactive=True, | ||
| ) | ||
| import_tags_button = gr.Button("Import", elem_id="open_folder") | ||
| ignore_load_tags_word_count = gr.Slider( | ||
| minimum=1, | ||
| maximum=100, | ||
| value=3, | ||
| step=1, | ||
| label="Ignore Imported Tags Above Word Count", | ||
| interactive=True, | ||
| ) | ||
|
|
||
| # Next/Prev section generator | ||
| def render_pagination(): | ||
| gr.Button("< Prev", elem_id="open_folder").click( | ||
| paginate, | ||
| inputs=[page, max_page, gr.Number(value=-1, visible=False)], | ||
| outputs=[page], | ||
| ) | ||
| page_count = gr.Label("Page 1", label="Page") | ||
| page_goto_text = gr.Textbox( | ||
| label="Goto page", | ||
| placeholder="Page Number", | ||
| interactive=True, | ||
| ) | ||
| gr.Button("Go >", elem_id="open_folder").click( | ||
| paginate_go, | ||
| inputs=[page_goto_text, max_page], | ||
| outputs=[page], | ||
| ) | ||
| gr.Button("Next >", elem_id="open_folder").click( | ||
| paginate, | ||
| inputs=[page, max_page, gr.Number(value=1, visible=False)], | ||
| outputs=[page], | ||
| ) | ||
| return page_count | ||
|
|
||
| with gr.Row(visible=False) as pagination_row1: | ||
| page_count1 = render_pagination() | ||
|
|
||
| # Images section | ||
| image_rows = [] | ||
| image_files = [] | ||
| image_images = [] | ||
| image_caption_texts = [] | ||
| image_tag_checks = [] | ||
| save_buttons = [] | ||
| for _ in range(IMAGES_TO_SHOW): | ||
|
|
||
| with gr.Group(): | ||
| with gr.Row(): | ||
| images_dir = gr.Dropdown(label="Image folder to caption", choices=[""] + list(list_dirs(default_images_dir)), value="", interactive=True, allow_custom_value=True) | ||
| create_refresh_button(images_dir, lambda: None, lambda: {"choices": list(list_dirs(images_dir.value or default_images_dir))}, "open_folder_small") | ||
| gr.Button("📂", elem_id="open_folder_small", elem_classes=["tool"], visible=not headless).click(get_folder_path, outputs=images_dir, show_progress=False) | ||
|
|
||
|
|
||
| with gr.Row(): | ||
| caption_ext = gr.Dropdown(label="Caption file extension", choices=[".cap", ".caption", ".txt"], value=".txt", interactive=True, allow_custom_value=True) | ||
| auto_save = gr.Checkbox(label="Autosave", value=True, interactive=True) |
|
Closing — the GUI change alone is insufficient since the |
Summary
Adds a Sort tags by confidence checkbox to the WD14 Captioning tab. When enabled, output tags are sorted by their model confidence score (highest first) before being written to the caption file, matching the ordering produced by the WaifuDiffusion online tagger.
--character_tags_firstand--always_first_tags/prefixBackend change required in sd-scripts
This PR wires the GUI flag. The
--sort_tags_by_confidenceargument also needs to be added tosd-scripts/finetune/tag_images_by_wd14_tagger.py. The required change is:In
setup_parser()— add after--character_tag_expand:In
run_batch()— replace the tag collection loop (lines ~255–272) with:Notes
always_first_tagsfield usesadd_pre_postfix(not--always_first_tags); this pre-existing behavior is unchangedCloses #3491