Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89bda6ba11 | ||
|
|
47d1c4f739 |
@@ -18,6 +18,12 @@ All notable changes to ComfyUI-AusBoss are documented here.
|
||||
Edit Lock's `edit_lock` output. It also gives a mask of what the edit
|
||||
changed and a one-line report.
|
||||
|
||||
- **New node: Text Lines.** It runs what comes after once for each line of
|
||||
your text. Type five prompts, one on each line, and one run makes five
|
||||
pictures. It also takes a list a text model wrote: empty lines are
|
||||
skipped, a leading number or bullet comes off, and **Limit** stops a list
|
||||
that ran long.
|
||||
|
||||
- **Mask by Touch: type a name to take it out of the mask.** The name field
|
||||
under the picture now has a sign in front. Click it to switch between **+**
|
||||
(add by name) and **−** (take away by name), then type "face" or "hair" and
|
||||
|
||||
@@ -357,6 +357,10 @@ Puts batch `b` after `a`. When the sizes match, nothing is resized. When they di
|
||||
|
||||
A text box that grows with the node and sends its text, unchanged, down a `STRING` wire. Use it for a prompt, caption, path or other text that several nodes share. Spaces and line breaks are kept. Ctrl + Up and Ctrl + Down change the weight of the selected words, as in ComfyUI's own prompt boxes.
|
||||
|
||||
### Text Lines
|
||||
|
||||
Runs what comes after once for each line of your text. Type five prompts, one on each line, and one run makes five pictures. It also takes a list another node wrote, skips empty lines, and can take a leading `1.` or `-` off each line. `number` counts the lines from 1, to give each its own seed.
|
||||
|
||||
### H3 Prompt Writer
|
||||
|
||||
Type what happens, one line for each moment, and leave a blank line to cut to a new place. Connect up to four pictures and give each one a job: a person, a place, an outfit, a thing or a style. Copy the moves from a video, keep a clip going, or add a voice or a song. A timeline on the node shows when each moment plays before you run it. It needs no extra model and you never see H3's prompt format. Connect a small text model and it fills in a one-line idea.
|
||||
|
||||
@@ -53,6 +53,7 @@ NODE_MODULES = [
|
||||
"node_select_frame",
|
||||
"node_show_text",
|
||||
"node_step_preview",
|
||||
"node_text_lines",
|
||||
"node_tiled_upscale",
|
||||
"node_video_crop_rotate_pad",
|
||||
"node_video_crop_rotate_pad_clip",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Text Lines
|
||||
|
||||
Runs what comes after once for each line of your text. Type five prompts,
|
||||
one on each line, press Run once, and you get five pictures.
|
||||
|
||||
Use it for a list of prompts you typed, or for a list another node wrote,
|
||||
such as a text model asked for "five different poses".
|
||||
|
||||
## How to use it
|
||||
|
||||
1. Type your lines in the box, or wire text into **text**.
|
||||
2. Wire **line** to where one prompt would go, such as a text encoder's
|
||||
prompt.
|
||||
3. Run. Every node after it runs once for each line, and a Save Image at the
|
||||
end saves one picture for each.
|
||||
|
||||
Empty lines are skipped. Models and pictures that do not depend on the line
|
||||
are loaded once, not once for each line.
|
||||
|
||||
## Controls
|
||||
|
||||
- The text box (`text`): One line for each run.
|
||||
- **Split at** (`split`): **each line** makes every line its own run.
|
||||
**blank line** keeps lines together and starts the next run at an empty
|
||||
line. Pick it when one prompt is several lines long.
|
||||
- **Numbering** (`remove_numbering`): **remove** takes a number or bullet
|
||||
off the start of each line, such as `1.`, `2)`, `-` or `[prompt 3]`.
|
||||
Lists a text model writes usually have them. **keep** leaves every line
|
||||
as written.
|
||||
- **Limit** (`limit`): The most lines to run. 0 runs them all. Set it when
|
||||
a text model might write more lines than you asked for.
|
||||
|
||||
## Outputs
|
||||
|
||||
- **line**: Each line, one at a time.
|
||||
- **number**: The line's place in the list, starting at 1. Add it to a seed
|
||||
with Math Expression (`a + b`) to give each line its own seed.
|
||||
- **count**: How many lines run.
|
||||
|
||||
## Notes
|
||||
|
||||
- With no lines at all, the run stops at this node and says so.
|
||||
- Every run uses the same seed unless you add **number** to it. Different
|
||||
prompts still give different pictures.
|
||||
- A line such as "2 women on a beach" or "1.5x zoom" keeps its number: only
|
||||
a number followed by a period, bracket or colon counts as numbering.
|
||||
|
||||
## Technical details
|
||||
|
||||
**line** and **number** are ComfyUI lists (`OUTPUT_IS_LIST`), the same way
|
||||
Image Folder hands out pictures. ComfyUI runs each later node once for each
|
||||
item and keeps the results in order. A node that takes a whole list at once,
|
||||
such as core Rebatch Images, gets all of them together.
|
||||
|
||||
Numbering that is removed: a number in square, round or CJK brackets with an
|
||||
optional word before it (`[prompt 1]`, `(2)`), a word and a number followed by
|
||||
a colon, period, bracket or dash (`Shot 3:`), a bare number followed by a
|
||||
period, bracket or colon that has no digit right after it, and the bullets
|
||||
`-`, `*` and `•` followed by a space. Only one marker is removed from each
|
||||
line.
|
||||
@@ -39,6 +39,7 @@ help panel.
|
||||
| Turn, crop and pad a video: one frame, or the whole clip | Video Crop + Rotate + Pad → Frame (`AUSBOSS_NODES_VideoCropRotatePad`), → Clip (`AUSBOSS_NODES_VideoCropRotatePadClip`) |
|
||||
| Change a clip's frame rate | Frame Interpolate (`AUSBOSS_NODES_FrameInterpolate`) |
|
||||
| Work with frame batches | Select Frame, Select Every Nth, Split Batch, Merge Batches (`AUSBOSS_NODES_SelectFrame`, `_SelectEveryNth`, `_SplitBatch`, `_MergeBatches`) |
|
||||
| Run a workflow once for each line of text, such as a list of prompts | Text Lines (`AUSBOSS_NODES_TextLines`) |
|
||||
| One shared seed, number or text | Seed, Integer, Float, Text (`AUSBOSS_NODES_Seed`, `_Integer`, `_Float`, `_Text`) |
|
||||
| Do maths on numbers | Math Expression (`AUSBOSS_NODES_MathExpression`) |
|
||||
| Show text on the node, or the picture while it is made | Show Text (`AUSBOSS_NODES_ShowText`), Step Preview (`AUSBOSS_NODES_StepPreview`) |
|
||||
|
||||
@@ -340,6 +340,17 @@ const CARDS = {
|
||||
rows: [{ widget: "text", kind: "textarea", placeholder: "Prompt, caption, or shared text", height: 100, grow: true,
|
||||
looks: { big: { height: 62, placeholder: "type here" } } }],
|
||||
},
|
||||
// One line for each run. The three settings under the box say where a run
|
||||
// ends, whether a leading "1." comes off, and how many lines run at most.
|
||||
AUSBOSS_NODES_TextLines: {
|
||||
minWidth: 320,
|
||||
rows: [
|
||||
{ widget: "text", kind: "textarea", placeholder: "One line for each run", height: 100, grow: true },
|
||||
{ widget: "split", label: "Split at", kind: "segment" },
|
||||
{ widget: "remove_numbering", label: "Numbering", onText: "remove", offText: "keep" },
|
||||
{ widget: "limit", label: "Limit" },
|
||||
],
|
||||
},
|
||||
// All the words go in the box, and the strip under it shows when each
|
||||
// moment plays. The pictures, the video, the voice and the song arrive on
|
||||
// sockets, and each picture's job sits under the words.
|
||||
|
||||
@@ -1298,6 +1298,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"AUSBOSS_NODES_TextLines": {
|
||||
"display_name": "逐行文本 🆎",
|
||||
"description": "文本的每一行都会让后面的节点运行一次。输入五条提示词,每行一条,运行一次就得到五张图。空行会跳过,行首的序号或项目符号可以去掉。",
|
||||
"inputs": {
|
||||
"text": {
|
||||
"name": "文本"
|
||||
},
|
||||
"split": {
|
||||
"name": "分隔方式"
|
||||
},
|
||||
"remove_numbering": {
|
||||
"name": "去掉序号"
|
||||
},
|
||||
"limit": {
|
||||
"name": "上限"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "行"
|
||||
},
|
||||
"1": {
|
||||
"name": "序号"
|
||||
},
|
||||
"2": {
|
||||
"name": "数量"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AUSBOSS_NODES_TiledUpscale": {
|
||||
"display_name": "分块放大 🆎",
|
||||
"description": "以百万像素设置成品尺寸,并把图片切成模型可以逐块重画的图块。一块就能装下的图片保持完整。把图块送进采样器,再和 stitcher 一起送进分块放大拼合 🆎。小图不会被重画到远超其细节的程度:最多重画到原百万像素的 max_growth 倍,再用普通方式放大。report 会说明对你的图片做了什么。",
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Cut a block of text into the items Text Lines runs one at a time.
|
||||
|
||||
Pure text handling with no ComfyUI imports, so it runs under the
|
||||
standard-library tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
SPLIT_EACH_LINE = "each line"
|
||||
SPLIT_BLANK_LINE = "blank line"
|
||||
SPLIT_MODES = (SPLIT_EACH_LINE, SPLIT_BLANK_LINE)
|
||||
|
||||
# The words a list writer puts before an item's number: "Prompt 3:", "Shot 2 -".
|
||||
# Not "image" or "picture": an edit prompt may open with "Image 1: ...".
|
||||
_LABEL = r"(?:prompt|shot|scene|line|step|item)"
|
||||
|
||||
# A marker at the start of an item that only counts or bullets it. A bare
|
||||
# number needs punctuation after it, and that punctuation must not be followed
|
||||
# by a digit, so "2 women on a beach", "1.5x zoom" and "10:30 at night" stay
|
||||
# whole. A bullet needs a space after it, so "*bold* text" stays whole.
|
||||
_MARKER = re.compile(
|
||||
r"""^\s*(?:
|
||||
[\[【(]\s*(?:%(label)s[\s_#-]*)?\d+\s*[\]】)]\s*[:.\-–—]? # [prompt 1] (2)
|
||||
| %(label)s[\s_#-]*\d+\s*[:.)\-–—] # Prompt 1: Shot 2 -
|
||||
| \#?\d+\s*[.):](?!\d) # 1. 2) #3:
|
||||
| [-*•·–—](?=\s|$) # - bullet
|
||||
)\s*"""
|
||||
% {"label": _LABEL},
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
|
||||
_BLANK_LINE = re.compile(r"\n[ \t]*(?:\n[ \t]*)+")
|
||||
|
||||
|
||||
def strip_marker(item: str) -> str:
|
||||
"""Take one leading number or bullet off an item."""
|
||||
return _MARKER.sub("", item, count=1).strip()
|
||||
|
||||
|
||||
def split_items(text, split=SPLIT_EACH_LINE, remove_numbering=True, limit=0) -> list[str]:
|
||||
"""The items of `text`, in order, with empty ones left out.
|
||||
|
||||
`split` picks where one item ends: at every line break, or only at an
|
||||
empty line (so an item can hold several lines). `remove_numbering` takes
|
||||
a leading "1." or "-" off each item. `limit` keeps the first so many;
|
||||
0 keeps them all.
|
||||
"""
|
||||
text = str(text or "").replace("\r\n", "\n").replace("\r", "\n")
|
||||
if split == SPLIT_BLANK_LINE:
|
||||
parts = _BLANK_LINE.split(text)
|
||||
else:
|
||||
parts = text.split("\n")
|
||||
|
||||
items = []
|
||||
for part in parts:
|
||||
item = part.strip()
|
||||
if remove_numbering:
|
||||
item = strip_marker(item)
|
||||
if item:
|
||||
items.append(item)
|
||||
|
||||
limit = int(limit or 0)
|
||||
if limit > 0:
|
||||
items = items[:limit]
|
||||
return items
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Text Lines 🆎 — run what comes after once for each line of text."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ._text_lines_helpers import SPLIT_EACH_LINE, SPLIT_MODES, split_items
|
||||
|
||||
MAX_LIMIT = 1000
|
||||
|
||||
|
||||
class AusBossTextLines:
|
||||
CATEGORY = "🆎 AusBoss/Utility"
|
||||
DESCRIPTION = (
|
||||
"Runs what comes after once for each line of your text. Type five "
|
||||
"prompts, one on each line, and one run makes five pictures. Empty "
|
||||
"lines are skipped, and a number or bullet at the start of a line "
|
||||
"can be taken off."
|
||||
)
|
||||
SEARCH_ALIASES = [
|
||||
"prompt list",
|
||||
"one prompt per line",
|
||||
"each line",
|
||||
"split text",
|
||||
"text to list",
|
||||
"batch prompts",
|
||||
"queue prompts",
|
||||
"ausboss",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": "One line for each run. Type it here, or wire in text another node wrote.",
|
||||
},
|
||||
),
|
||||
"split": (
|
||||
list(SPLIT_MODES),
|
||||
{
|
||||
"default": SPLIT_EACH_LINE,
|
||||
"tooltip": (
|
||||
"each line: every line is its own run. blank line: lines stay "
|
||||
"together, and an empty line starts the next run."
|
||||
),
|
||||
},
|
||||
),
|
||||
"remove_numbering": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": True,
|
||||
"tooltip": (
|
||||
'On: a number or bullet at the start is taken off, like "1.", "-" '
|
||||
'or "[prompt 2]". Off: every line stays as written.'
|
||||
),
|
||||
},
|
||||
),
|
||||
"limit": (
|
||||
"INT",
|
||||
{
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": MAX_LIMIT,
|
||||
"step": 1,
|
||||
"tooltip": "The most lines to run. 0 runs them all.",
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "INT", "INT")
|
||||
RETURN_NAMES = ("line", "number", "count")
|
||||
OUTPUT_IS_LIST = (True, True, False)
|
||||
OUTPUT_TOOLTIPS = (
|
||||
"Each line, one at a time. What comes after runs once for each.",
|
||||
"The line's place in the list, starting at 1. Add it to a seed to give each run its own.",
|
||||
"How many lines run.",
|
||||
)
|
||||
FUNCTION = "split"
|
||||
|
||||
def split(self, text, split=SPLIT_EACH_LINE, remove_numbering=True, limit=0):
|
||||
items = split_items(text, split, remove_numbering, limit)
|
||||
if not items:
|
||||
raise ValueError(
|
||||
"Text Lines has no lines to run. Type at least one line, or check the node that writes its text."
|
||||
)
|
||||
return (items, list(range(1, len(items) + 1)), len(items))
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"AUSBOSS_NODES_TextLines": AusBossTextLines,
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"AUSBOSS_NODES_TextLines": "Text Lines 🆎",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
@@ -94,6 +94,7 @@ PUBLIC_NODE_IDS = {
|
||||
"AUSBOSS_NODES_StitchInpaint",
|
||||
"AUSBOSS_NODES_TiledUpscaleStitch",
|
||||
"AUSBOSS_NODES_Text",
|
||||
"AUSBOSS_NODES_TextLines",
|
||||
"AUSBOSS_NODES_TiledUpscale",
|
||||
"AUSBOSS_NODES_VideoCropRotatePad",
|
||||
"AUSBOSS_NODES_VideoCropRotatePadClip",
|
||||
|
||||
Vendored
+23
@@ -1036,6 +1036,29 @@
|
||||
]
|
||||
]
|
||||
},
|
||||
"AUSBOSS_NODES_TextLines": {
|
||||
"required": [
|
||||
"text",
|
||||
"split",
|
||||
"remove_numbering",
|
||||
"limit"
|
||||
],
|
||||
"optional": [],
|
||||
"outputs": [
|
||||
[
|
||||
"STRING",
|
||||
"line"
|
||||
],
|
||||
[
|
||||
"INT",
|
||||
"number"
|
||||
],
|
||||
[
|
||||
"INT",
|
||||
"count"
|
||||
]
|
||||
]
|
||||
},
|
||||
"AUSBOSS_NODES_TiledUpscale": {
|
||||
"required": [
|
||||
"image",
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
if "nodes" in sys.modules and not hasattr(sys.modules["nodes"], "__path__"):
|
||||
del sys.modules["nodes"]
|
||||
|
||||
from nodes._text_lines_helpers import split_items, strip_marker
|
||||
from nodes.node_text_lines import AusBossTextLines
|
||||
|
||||
|
||||
class SplitItemsTests(unittest.TestCase):
|
||||
def test_each_line_is_an_item_and_empty_lines_are_skipped(self):
|
||||
text = "a red car\n\n \na blue boat \r\n\tgreen hills\n"
|
||||
self.assertEqual(split_items(text), ["a red car", "a blue boat", "green hills"])
|
||||
|
||||
def test_blank_line_keeps_lines_together(self):
|
||||
text = "first line\nsecond line\n\n\nnext item\n \nlast"
|
||||
self.assertEqual(
|
||||
split_items(text, "blank line"),
|
||||
["first line\nsecond line", "next item", "last"],
|
||||
)
|
||||
|
||||
def test_limit_keeps_the_first_items(self):
|
||||
self.assertEqual(split_items("a\nb\nc\nd", limit=2), ["a", "b"])
|
||||
self.assertEqual(split_items("a\nb", limit=0), ["a", "b"])
|
||||
self.assertEqual(split_items("a\nb", limit=9), ["a", "b"])
|
||||
|
||||
def test_numbers_and_bullets_come_off(self):
|
||||
cases = {
|
||||
"1. close-up, soft smile": "close-up, soft smile",
|
||||
"2) medium shot": "medium shot",
|
||||
"#3: from above": "from above",
|
||||
"- a bullet": "a bullet",
|
||||
"* a star": "a star",
|
||||
"• a dot": "a dot",
|
||||
"[prompt 4] from the side": "from the side",
|
||||
"【prompt_5】 looking back": "looking back",
|
||||
"(6) arms crossed": "arms crossed",
|
||||
"Prompt 7: laughing": "laughing",
|
||||
"Shot 8 - wide": "wide",
|
||||
"12. twelve": "twelve",
|
||||
}
|
||||
for line, want in cases.items():
|
||||
self.assertEqual(strip_marker(line), want, line)
|
||||
|
||||
def test_text_that_only_starts_like_a_marker_stays(self):
|
||||
for line in [
|
||||
"2 women on a beach",
|
||||
"1.5x zoom on the face",
|
||||
"10:30 at night, a diner",
|
||||
"3D render of a robot",
|
||||
"*bold* light from the left",
|
||||
"-90 degrees, seen from below",
|
||||
"1920s street, a man in a hat",
|
||||
"shot from below",
|
||||
"Image 1: the woman, in the coat from image 2",
|
||||
]:
|
||||
self.assertEqual(strip_marker(line), line, line)
|
||||
|
||||
def test_numbering_stays_when_asked(self):
|
||||
self.assertEqual(split_items("1. a\n- b", remove_numbering=False), ["1. a", "- b"])
|
||||
|
||||
def test_a_line_that_is_only_a_marker_is_skipped(self):
|
||||
self.assertEqual(split_items("1.\n2. real\n-"), ["real"])
|
||||
|
||||
def test_nothing_gives_nothing(self):
|
||||
self.assertEqual(split_items(""), [])
|
||||
self.assertEqual(split_items(None), [])
|
||||
self.assertEqual(split_items("\n \n"), [])
|
||||
|
||||
|
||||
class TextLinesNodeTests(unittest.TestCase):
|
||||
def test_outputs_lines_numbers_and_count(self):
|
||||
lines, numbers, count = AusBossTextLines().split("1. one\n2. two\n\n3. three")
|
||||
self.assertEqual(lines, ["one", "two", "three"])
|
||||
self.assertEqual(numbers, [1, 2, 3])
|
||||
self.assertEqual(count, 3)
|
||||
|
||||
def test_the_line_and_number_outputs_are_lists(self):
|
||||
self.assertEqual(AusBossTextLines.OUTPUT_IS_LIST, (True, True, False))
|
||||
self.assertEqual(AusBossTextLines.RETURN_NAMES, ("line", "number", "count"))
|
||||
|
||||
def test_no_lines_stops_the_run_with_plain_words(self):
|
||||
with self.assertRaises(ValueError) as caught:
|
||||
AusBossTextLines().split(" \n")
|
||||
self.assertIn("no lines to run", str(caught.exception))
|
||||
|
||||
def test_inputs_and_their_order(self):
|
||||
inputs = AusBossTextLines.INPUT_TYPES()
|
||||
self.assertEqual(list(inputs), ["required"])
|
||||
self.assertEqual(list(inputs["required"]), ["text", "split", "remove_numbering", "limit"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user