Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 187940b45f | |||
| 718da9a68d | |||
| 030a1255e1 | |||
| 842d3580f5 | |||
| 15b28b422f | |||
| c74798d80f | |||
| 7f6bf0ffd8 | |||
| d546061959 | |||
| 42857e938e | |||
| 221659a58d | |||
| a4b4dae8cf | |||
| e9bd9c45ca | |||
| 4f97057fc4 | |||
| 4de00bcc9d | |||
| b6314a246a | |||
| a539055565 | |||
| 7a60da23f0 | |||
| 9b69cc8505 | |||
| 613fada952 | |||
| 48a2afc951 | |||
| ec6cc7265c | |||
| 1f9544233e | |||
| de6615c024 | |||
| eb1bdbf305 | |||
| 1ca9c95bfe | |||
| 4a3610fbc9 | |||
| 307ffdba3b | |||
| 3c7ccbb711 | |||
| 007386aae3 | |||
| 098721504d | |||
| a50b9272fe | |||
| 9a4324e08e | |||
| 3cd34f650e | |||
| 2b41a82869 | |||
| 658743d876 | |||
| 08627be954 | |||
| c6f0fc34af | |||
| 80e7e6e156 | |||
| 7778a5f31f | |||
| f91953f12b | |||
| cac4fe47cd | |||
| 5ca5f1b858 | |||
| 29ca3ba369 | |||
| 6a65f7d35c | |||
| 867916ee51 | |||
| 8ae689f0e7 | |||
| a94cb9f8f1 | |||
| 727aea6307 | |||
| 8c012ee560 | |||
| 7e0c9ed13b | |||
| 8a1a34ad08 | |||
| 2b9e880b11 | |||
| 9668bd1709 | |||
| c59c9947b2 | |||
| 1950ce7bbf | |||
| f110ee6a89 | |||
| 7c615bdf7b | |||
| 0c62df36de | |||
| ed67c9ba7b | |||
| 4714e23dc8 | |||
| 83d661919f | |||
| 7cd2d48e6a | |||
| 3cb44af410 | |||
| 87f3645115 | |||
| 96ff37a5a0 | |||
| 9cd1f03bfe | |||
| c69274d2ee | |||
| 002c3b79d4 | |||
| ff6195473b | |||
| d0f2670d9c | |||
| bb7df8ad77 | |||
| 607c2b8751 | |||
| 3d0a8cace8 | |||
| e7bc227c6f | |||
| 728d3e559c | |||
| 5ae2f31a20 | |||
| a8d69083cd | |||
| 29e5e65e5f | |||
| ac4c50bf34 | |||
| 91b8842cb2 | |||
| 0a0951e5e5 | |||
| 95dc8939b6 | |||
| d724e4518a | |||
| c3bce91541 | |||
| 81d69c753e | |||
| ec79257613 | |||
| 7bc08ada47 | |||
| c7e4bdc373 | |||
| 0289a94153 | |||
| c34886b362 | |||
| 4fdef3875b | |||
| 333fa5eae6 | |||
| 6f6afb4d22 | |||
| 928f55d2c3 | |||
| bd3adfcd5a | |||
| c4d5477bf9 | |||
| 194eb06465 | |||
| 17c6d34784 | |||
| f811c02641 | |||
| 75a71a2df6 | |||
| 63e8489fb2 | |||
| 616d1132ff | |||
| 58f74e44e5 | |||
| 6ff3b0cbd5 | |||
| 811ff86f72 | |||
| 2dbaaeddb3 | |||
| e5e194c68b | |||
| 2a5e565ce7 | |||
| 8eb3f6d394 | |||
| 3599a334be | |||
| 05c84c6b83 | |||
| 9a2e5db041 | |||
| c67be207ab | |||
| 1ee0b6e91a | |||
| 837299be6c | |||
| 84c369c190 | |||
| 9a5809deaa |
+46
-72
@@ -1,81 +1,55 @@
|
|||||||
# Improvement Path
|
# Improvement Path
|
||||||
|
|
||||||
## Done
|
This file is a current-state improvement map. It should stay concrete: add work
|
||||||
|
here when it reflects an observed workflow problem, a new metadata path, or a
|
||||||
|
specific generated prompt failure.
|
||||||
|
|
||||||
- ComfyUI prompt node.
|
## Current Baseline
|
||||||
- JSON-defined main categories and subcategories.
|
|
||||||
- Compositional item generators with `item_templates` and `item_axes`.
|
|
||||||
- Softcore/custom clothing categories.
|
|
||||||
- Explicit erotic clothing category.
|
|
||||||
- Hardcore sexual-pose category.
|
|
||||||
- Configurable cast counts with `women_count` and `men_count`.
|
|
||||||
- Per-axis seed control through `SxCP Seed Control`.
|
|
||||||
- Cast-aware filtering for subcategories, templates, and axis values.
|
|
||||||
- Role graph generation for configured hardcore casts.
|
|
||||||
|
|
||||||
## Highest-Value Next Steps
|
- Categories, subcategories, item templates, scene pools, expression pools, and
|
||||||
|
composition pools are JSON-driven.
|
||||||
|
- New pool content can be added through extension data instead of Python edits.
|
||||||
|
- Prompt rows and Insta/OF pairs carry structured metadata for Krea2, SDXL, and
|
||||||
|
caption routes.
|
||||||
|
- Krea2, SDXL, and caption formatting prefer metadata over raw prompt text.
|
||||||
|
- Seed behavior is axis-based: global seed, seed control, seed locker,
|
||||||
|
per-character slot seed, and deterministic route simulation are available.
|
||||||
|
- Character slots support chained casts, saved profiles, side-node
|
||||||
|
characteristics, per-character expression, and per-character clothing state.
|
||||||
|
- Insta/OF pairs can generate softcore and hardcore prompts from a shared cast,
|
||||||
|
scene, and camera configuration.
|
||||||
|
- Hardcore position/action routing is split by action family and configurable
|
||||||
|
through position-pool and action-filter nodes.
|
||||||
|
- Camera is first-class: manual camera control, orbit/Qwen camera translation,
|
||||||
|
POV policy, and location-aware scene-camera adapters are separate concerns.
|
||||||
|
- Utility nodes cover index switching, loop control, accumulation, image
|
||||||
|
preview management, persistent text preview, SDXL buckets, and Krea2
|
||||||
|
resolution selection.
|
||||||
|
- `tools/prompt_map_audit.py`, `tools/prompt_route_simulation.py`, and
|
||||||
|
`tools/prompt_smoke.py` cover the main registration, metadata, formatter, and
|
||||||
|
seed-control drift paths.
|
||||||
|
|
||||||
1. Explicitness preset
|
## Improvement Rule
|
||||||
|
|
||||||
Add a node input like:
|
Do not add broad checks or generic prompt rewrites just because an issue is
|
||||||
|
possible. Improve a path when one of these is true:
|
||||||
|
|
||||||
- `softcore`
|
- A generated prompt shows concrete noise, contradiction, or hidden logic drift.
|
||||||
- `nude`
|
- A workflow action is awkward in ComfyUI and needs a better node surface.
|
||||||
- `explicit`
|
- A new metadata field, node, category family, formatter route, or location
|
||||||
- `hardcore`
|
adapter is added.
|
||||||
|
- A formatter starts relying on raw prompt text where structured metadata should
|
||||||
|
exist.
|
||||||
|
|
||||||
Then categories can share the same cast/person/scene system while swapping
|
## Concrete Next Work
|
||||||
the pose/content pools and negative prompts.
|
|
||||||
|
|
||||||
2. Anatomy clarity axis
|
1. Add route-level smoke fixtures only for observed generated edge cases or new
|
||||||
|
metadata fields that affect Krea2, SDXL, or caption output.
|
||||||
Add a controlled axis for visual clarity:
|
2. Extend `scene_camera_adapters.py` one location family at a time, after the
|
||||||
|
actual generated prompts show the location needs camera-aware wording.
|
||||||
- full-body view
|
3. Add characteristic side nodes only when repeated manual slot fields become
|
||||||
- hips-focused view
|
workflow friction.
|
||||||
- genital-contact view
|
4. Tune hardcore, softcore, SDXL, or caption wording only from real output
|
||||||
- face-and-body view
|
examples, not from speculative prompt rules.
|
||||||
- mirror view
|
5. When adding a node/path, update the route map and audit coverage in the same
|
||||||
|
change so organization stays discoverable.
|
||||||
This helps hardcore outputs read as sex scenes instead of vague tangled
|
|
||||||
bodies.
|
|
||||||
|
|
||||||
3. Outfit and pose compatibility
|
|
||||||
|
|
||||||
Hardcore pose categories should optionally pull from erotic clothing or nude
|
|
||||||
accessory categories. Add an input or template field for:
|
|
||||||
|
|
||||||
- clothed sex
|
|
||||||
- lingerie sex
|
|
||||||
- nude sex
|
|
||||||
- fetishwear sex
|
|
||||||
- wet/shower sex
|
|
||||||
|
|
||||||
4. More seed/reroll utility nodes
|
|
||||||
|
|
||||||
Add tiny helper nodes:
|
|
||||||
|
|
||||||
- `SxCP Reroll Pose Seed`
|
|
||||||
- `SxCP Reroll Scene Seed`
|
|
||||||
- `SxCP Reroll Person Seed`
|
|
||||||
|
|
||||||
These can output a modified `seed_config` while preserving the other locked
|
|
||||||
seeds.
|
|
||||||
|
|
||||||
5. Validation and preview tools
|
|
||||||
|
|
||||||
Add a local validator that reports:
|
|
||||||
|
|
||||||
- category and subcategory counts
|
|
||||||
- template placeholder errors
|
|
||||||
- axis size and variation count
|
|
||||||
- impossible cast/template combinations
|
|
||||||
- missing scene/pose/expression pools
|
|
||||||
|
|
||||||
## Hardcore-Specific Improvement Order
|
|
||||||
|
|
||||||
1. Split hardcore into act families with deeper compatibility rules.
|
|
||||||
2. Add explicitness preset and prompt-strength controls.
|
|
||||||
3. Add anatomy/camera clarity axis.
|
|
||||||
4. Add outfit-state control for nude/lingerie/fetish/clothed sex.
|
|
||||||
5. Add validation so impossible prompts are caught before ComfyUI generation.
|
|
||||||
|
|||||||
@@ -38,6 +38,23 @@ The node is registered as:
|
|||||||
- `prompt_builder / SxCP Krea2 Formatter`
|
- `prompt_builder / SxCP Krea2 Formatter`
|
||||||
- `prompt_builder / SxCP Insta/OF Options`
|
- `prompt_builder / SxCP Insta/OF Options`
|
||||||
- `prompt_builder / SxCP Insta/OF Prompt Pair`
|
- `prompt_builder / SxCP Insta/OF Prompt Pair`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Start`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Cast`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Character`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Wardrobe`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Location`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Set Dressing`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Blocking`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Action`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Performance`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Camera`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Composition`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Lighting`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Branch Pair`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Softcore Branch Options`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Hardcore Branch Options`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Output`
|
||||||
|
- `prompt_builder / v2_scene / SxCP Scene Pair Output`
|
||||||
|
|
||||||
It outputs:
|
It outputs:
|
||||||
|
|
||||||
@@ -64,10 +81,14 @@ node. For cleaner workflows, use the split nodes:
|
|||||||
`men_weights=0.5,0.3` means 50% no man and 30% one man.
|
`men_weights=0.5,0.3` means 50% no man and 30% one man.
|
||||||
- `SxCP Location Pool` outputs `location_config`. `replace` uses only the
|
- `SxCP Location Pool` outputs `location_config`. `replace` uses only the
|
||||||
selected/custom location pool; `add` keeps the category's own locations and
|
selected/custom location pool; `add` keeps the category's own locations and
|
||||||
adds yours. Custom lines can be plain location text, or `slug: location text`.
|
adds yours. Custom lines can be plain location text, `slug: location text`, or
|
||||||
|
one-line JSON objects/arrays. JSON location entries preserve metadata such as
|
||||||
|
inline `camera_profile` / `scene_camera_profile`.
|
||||||
- `SxCP Composition Pool` outputs `composition_config` to control framing
|
- `SxCP Composition Pool` outputs `composition_config` to control framing
|
||||||
separately from location. Use it when category framing mentions unrelated
|
separately from location. Use it when category framing mentions unrelated
|
||||||
outfit-check details such as shoes, bags, or mirror poses.
|
outfit-check details such as shoes, bags, or mirror poses. Custom composition
|
||||||
|
lines can also be one-line JSON objects/arrays when metadata needs to travel
|
||||||
|
with the selected composition.
|
||||||
- `SxCP Location Theme` outputs matched `location_config` and
|
- `SxCP Location Theme` outputs matched `location_config` and
|
||||||
`composition_config`. Themes such as `classical_library`,
|
`composition_config`. Themes such as `classical_library`,
|
||||||
`semi_public_affair`, `hotel_corridor`, `parking_garage`, and
|
`semi_public_affair`, `hotel_corridor`, `parking_garage`, and
|
||||||
@@ -97,6 +118,20 @@ The practical compact workflow is:
|
|||||||
`Woman Slot` / `Man Slot`, and `Character Profile`
|
`Woman Slot` / `Man Slot`, and `Character Profile`
|
||||||
into `Prompt Builder From Configs`.
|
into `Prompt Builder From Configs`.
|
||||||
|
|
||||||
|
## Scene-Chain v2 Nodes
|
||||||
|
|
||||||
|
The v2 scene nodes are an additive workflow surface. They pass one structured
|
||||||
|
`SXCP_SCENE` object through cast, character, wardrobe, location, set dressing,
|
||||||
|
blocking, action, performance, camera, composition, and lighting layers. Use
|
||||||
|
`SxCP Scene Output` for a single prompt, or split a shared scene with
|
||||||
|
`SxCP Scene Branch Pair`, refine it with `SxCP Softcore Branch Options` and
|
||||||
|
`SxCP Hardcore Branch Options`, then render both sides through
|
||||||
|
`SxCP Scene Pair Output`.
|
||||||
|
|
||||||
|
The current v2 output nodes intentionally reuse the existing builder,
|
||||||
|
Insta/OF pair, and formatter metadata routes. This keeps old workflows working
|
||||||
|
while giving new workflows a cleaner movie-scene structure.
|
||||||
|
|
||||||
An importable default workflow is included at
|
An importable default workflow is included at
|
||||||
`examples/default_task_lanes_workflow.json`. It is laid out by task instead of
|
`examples/default_task_lanes_workflow.json`. It is laid out by task instead of
|
||||||
as one long chain:
|
as one long chain:
|
||||||
@@ -331,11 +366,11 @@ prompt result. Manual fields and explicitly fixed per-axis or character-slot
|
|||||||
seeds still override the global seed for those parts.
|
seeds still override the global seed for those parts.
|
||||||
|
|
||||||
`SxCP Seed Control` outputs `seed_config`, which can be connected to the prompt
|
`SxCP Seed Control` outputs `seed_config`, which can be connected to the prompt
|
||||||
builder's optional `seed_config` input. When an axis is set to `random`, the
|
builder's optional `seed_config` input. It also outputs a `summary` string with
|
||||||
visible seed value is materialized before the workflow queues, and that exact
|
the resolved value for every axis. When an axis is set to `random`, the widget
|
||||||
value is used for the queued prompt. The mode returns to `random` after queueing
|
can stay at `-1`, but the emitted `seed_config` and `summary` contain the
|
||||||
so the next run can reroll. Use `Lock Random Seeds Now` on the node when you want
|
concrete seed used for that queued prompt. Use `Lock Random Seeds Now` on the
|
||||||
to convert the current random axes into fixed reusable seeds.
|
node when you want to convert the current random axes into fixed reusable seeds.
|
||||||
|
|
||||||
`SxCP Seed Locker` is the fast version for iteration. Set `base_seed` to a seed
|
`SxCP Seed Locker` is the fast version for iteration. Set `base_seed` to a seed
|
||||||
you like, choose one `reroll_axis`, and connect its `seed_config`. All other
|
you like, choose one `reroll_axis`, and connect its `seed_config`. All other
|
||||||
@@ -413,13 +448,26 @@ The translator accepts the Qwen labels such as `front-right quarter view`,
|
|||||||
as the native camera nodes. `suppress_phone_visibility` is enabled by default so
|
as the native camera nodes. `suppress_phone_visibility` is enabled by default so
|
||||||
generic Qwen camera views do not add `phone hidden` or other phone wording.
|
generic Qwen camera views do not add `phone hidden` or other phone wording.
|
||||||
|
|
||||||
For coworking-style locations, the prompt builder also uses the translated
|
For camera-aware locations, the prompt builder also uses the translated camera
|
||||||
camera geometry to add a location-aware framing sentence. It currently targets
|
geometry to add a location-aware framing sentence. It currently has scene
|
||||||
`coworking lounge`, `business cafe`, and empty office scenes: front/side/back
|
profiles for coworking/business-office spaces, classical library/book-stack
|
||||||
views, zoom, and elevation change which desks, windows, laptop tables, glass
|
spaces, and semi-public repeating-structure locations such as hotel corridors,
|
||||||
partitions, counters, or office rows are kept visible. In male-POV setups this
|
parking garages, archives, laundromats, station lockers, backstage halls, wine
|
||||||
becomes a first-person spatial description and the external camera sentence is
|
cellars, nightclub back halls, and restaurant booths. Front/side/back views,
|
||||||
suppressed.
|
zoom, and elevation change which desks, windows, partitions, bookshelves,
|
||||||
|
corridors, pillars, shelves, tables, lamps, or aisles are kept visible. In
|
||||||
|
male-POV setups this becomes a first-person spatial description and the
|
||||||
|
external camera sentence is suppressed.
|
||||||
|
Rows keep the selected `scene_entry`, `location_theme`, `scene_theme`,
|
||||||
|
`composition_entry`, `composition_theme`, and `scene_camera_profile_key` in
|
||||||
|
`metadata_json` so location/camera behavior can be debugged without guessing
|
||||||
|
from prompt text alone.
|
||||||
|
When camera-aware profile routing runs, explicit `scene_camera_profile_key` and
|
||||||
|
theme metadata are used before fallback text matching.
|
||||||
|
Advanced scene entries may also include an inline `camera_profile` /
|
||||||
|
`scene_camera_profile` object with `layout_label`, `foreground`, `midground`,
|
||||||
|
`background`, and optional composition text, so custom location packs can define
|
||||||
|
their own camera behavior.
|
||||||
|
|
||||||
`SxCP SDXL Formatter` rewrites prompt builder output or `metadata_json` into
|
`SxCP SDXL Formatter` rewrites prompt builder output or `metadata_json` into
|
||||||
comma-tag SDXL/Pony-style prompts. Connect `metadata_json` when possible so
|
comma-tag SDXL/Pony-style prompts. Connect `metadata_json` when possible so
|
||||||
@@ -445,13 +493,17 @@ cleanup.
|
|||||||
|
|
||||||
When connected to `SxCP Insta/OF Prompt Pair` metadata, the naturalizer emits a
|
When connected to `SxCP Insta/OF Prompt Pair` metadata, the naturalizer emits a
|
||||||
single combined natural caption with the shared descriptor plus separate
|
single combined natural caption with the shared descriptor plus separate
|
||||||
softcore and hardcore version descriptions. It uses the final selected
|
softcore and hardcore side descriptions. It uses the final selected
|
||||||
expression and composition from the generated rows, including any expression
|
expression and composition from the generated rows, including any expression
|
||||||
pool and intensity settings.
|
pool and intensity settings.
|
||||||
|
Set `target=softcore` or `target=hardcore` to emit only one side of the pair for
|
||||||
|
training captions or formatter chains.
|
||||||
|
|
||||||
Naturalizer controls:
|
Naturalizer controls:
|
||||||
|
|
||||||
- `input_hint`: `auto`, `metadata_json`, or `caption_or_prompt`.
|
- `input_hint`: `auto`, `metadata_json`, or `caption_or_prompt`.
|
||||||
|
- `target`: `auto` keeps the combined pair caption; `single`, `softcore`, and
|
||||||
|
`hardcore` mirror the formatter target controls.
|
||||||
- `caption_profile`: `manual_controls` keeps the detail/style/trigger widgets
|
- `caption_profile`: `manual_controls` keeps the detail/style/trigger widgets
|
||||||
authoritative; `training_concise`, `training_dense`, and `browsing` apply
|
authoritative; `training_concise`, `training_dense`, and `browsing` apply
|
||||||
preset caption behavior.
|
preset caption behavior.
|
||||||
@@ -866,10 +918,12 @@ axis has its own mode plus seed value:
|
|||||||
- `follow_main`: always follows the final generator's main `seed` input and
|
- `follow_main`: always follows the final generator's main `seed` input and
|
||||||
ignores the entered axis seed.
|
ignores the entered axis seed.
|
||||||
- `fixed`: always uses the entered axis seed.
|
- `fixed`: always uses the entered axis seed.
|
||||||
- `random`: generates a fresh visible axis seed when the workflow queues.
|
- `random`: generates a fresh resolved axis seed when the workflow queues.
|
||||||
|
|
||||||
The `Lock Random Seeds Now` button turns every current `random` axis into a
|
The `summary` output lists the resolved value for every axis, including random
|
||||||
visible concrete seed and switches those axes to `fixed`.
|
axes whose visible widget value remains `-1`. The `Lock Random Seeds Now` button
|
||||||
|
turns every current `random` axis into a visible concrete seed and switches
|
||||||
|
those axes to `fixed`.
|
||||||
|
|
||||||
For exact prompt reproduction, `SxCP Global Seed` is the shortest path:
|
For exact prompt reproduction, `SxCP Global Seed` is the shortest path:
|
||||||
|
|
||||||
|
|||||||
+11
@@ -24,6 +24,7 @@ SXCP_HARDCORE_POSITION_CONFIG = "SXCP_HARDCORE_POSITION_CONFIG"
|
|||||||
SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"
|
SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"
|
||||||
SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT"
|
SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT"
|
||||||
SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE"
|
SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE"
|
||||||
|
SXCP_SCENE = "SXCP_SCENE"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .node_tooltips import install_input_tooltips as _install_input_tooltips
|
from .node_tooltips import install_input_tooltips as _install_input_tooltips
|
||||||
@@ -72,6 +73,10 @@ try:
|
|||||||
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
|
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
|
||||||
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
|
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
)
|
)
|
||||||
|
from .node_scene import (
|
||||||
|
NODE_CLASS_MAPPINGS as SCENE_NODE_CLASS_MAPPINGS,
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS as SCENE_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
|
)
|
||||||
from .server_routes import (
|
from .server_routes import (
|
||||||
accumulator_delete_payload,
|
accumulator_delete_payload,
|
||||||
accumulator_list_payload,
|
accumulator_list_payload,
|
||||||
@@ -120,6 +125,10 @@ except ImportError:
|
|||||||
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
|
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
|
||||||
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
|
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
)
|
)
|
||||||
|
from node_scene import (
|
||||||
|
NODE_CLASS_MAPPINGS as SCENE_NODE_CLASS_MAPPINGS,
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS as SCENE_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
|
)
|
||||||
from server_routes import (
|
from server_routes import (
|
||||||
accumulator_delete_payload,
|
accumulator_delete_payload,
|
||||||
accumulator_list_payload,
|
accumulator_list_payload,
|
||||||
@@ -186,6 +195,7 @@ NODE_CLASS_MAPPINGS.update(FORMATTER_NODE_CLASS_MAPPINGS)
|
|||||||
NODE_CLASS_MAPPINGS.update(INSTA_NODE_CLASS_MAPPINGS)
|
NODE_CLASS_MAPPINGS.update(INSTA_NODE_CLASS_MAPPINGS)
|
||||||
NODE_CLASS_MAPPINGS.update(ROUTE_CONFIG_NODE_CLASS_MAPPINGS)
|
NODE_CLASS_MAPPINGS.update(ROUTE_CONFIG_NODE_CLASS_MAPPINGS)
|
||||||
NODE_CLASS_MAPPINGS.update(PROFILE_FILTER_NODE_CLASS_MAPPINGS)
|
NODE_CLASS_MAPPINGS.update(PROFILE_FILTER_NODE_CLASS_MAPPINGS)
|
||||||
|
NODE_CLASS_MAPPINGS.update(SCENE_NODE_CLASS_MAPPINGS)
|
||||||
NODE_CLASS_MAPPINGS.update(LOOP_NODE_CLASS_MAPPINGS)
|
NODE_CLASS_MAPPINGS.update(LOOP_NODE_CLASS_MAPPINGS)
|
||||||
_install_input_tooltips(NODE_CLASS_MAPPINGS)
|
_install_input_tooltips(NODE_CLASS_MAPPINGS)
|
||||||
|
|
||||||
@@ -199,6 +209,7 @@ NODE_DISPLAY_NAME_MAPPINGS.update(FORMATTER_NODE_DISPLAY_NAME_MAPPINGS)
|
|||||||
NODE_DISPLAY_NAME_MAPPINGS.update(INSTA_NODE_DISPLAY_NAME_MAPPINGS)
|
NODE_DISPLAY_NAME_MAPPINGS.update(INSTA_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
NODE_DISPLAY_NAME_MAPPINGS.update(ROUTE_CONFIG_NODE_DISPLAY_NAME_MAPPINGS)
|
NODE_DISPLAY_NAME_MAPPINGS.update(ROUTE_CONFIG_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
NODE_DISPLAY_NAME_MAPPINGS.update(PROFILE_FILTER_NODE_DISPLAY_NAME_MAPPINGS)
|
NODE_DISPLAY_NAME_MAPPINGS.update(PROFILE_FILTER_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS.update(SCENE_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
NODE_DISPLAY_NAME_MAPPINGS.update(LOOP_NODE_DISPLAY_NAME_MAPPINGS)
|
NODE_DISPLAY_NAME_MAPPINGS.update(LOOP_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
|
|
||||||
WEB_DIRECTORY = "./web"
|
WEB_DIRECTORY = "./web"
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PromptFromConfigsRequest:
|
||||||
|
row_number: int
|
||||||
|
start_index: int
|
||||||
|
seed: int
|
||||||
|
category_config: str | dict[str, Any] | None = ""
|
||||||
|
cast_config: str | dict[str, Any] | None = ""
|
||||||
|
generation_profile: str | dict[str, Any] | None = ""
|
||||||
|
filter_config: str | dict[str, Any] | None = ""
|
||||||
|
seed_config: str | dict[str, Any] | None = ""
|
||||||
|
camera_config: str | dict[str, Any] | None = ""
|
||||||
|
character_profile: str | dict[str, Any] | None = ""
|
||||||
|
character_cast: str | dict[str, Any] | list[Any] | None = ""
|
||||||
|
hardcore_position_config: str | dict[str, Any] | None = ""
|
||||||
|
location_config: str | dict[str, Any] | None = ""
|
||||||
|
composition_config: str | dict[str, Any] | None = ""
|
||||||
|
extra_positive: str = ""
|
||||||
|
extra_negative: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PromptFromConfigsRoute:
|
||||||
|
row: dict[str, Any]
|
||||||
|
category: str
|
||||||
|
subcategory: str
|
||||||
|
cast: dict[str, Any]
|
||||||
|
profile: dict[str, Any]
|
||||||
|
filters: dict[str, Any]
|
||||||
|
build_kwargs: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PromptFromConfigsDependencies:
|
||||||
|
parse_category_config: Callable[[str | dict[str, Any] | None], tuple[str, str]]
|
||||||
|
parse_cast_config: Callable[[str | dict[str, Any] | None], dict[str, Any]]
|
||||||
|
parse_generation_profile: Callable[[str | dict[str, Any] | None], dict[str, Any]]
|
||||||
|
parse_filter_config: Callable[[str | dict[str, Any] | None], dict[str, Any]]
|
||||||
|
build_prompt: Callable[..., dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
def build_prompt_from_configs_result(
|
||||||
|
request: PromptFromConfigsRequest,
|
||||||
|
deps: PromptFromConfigsDependencies,
|
||||||
|
) -> PromptFromConfigsRoute:
|
||||||
|
category, subcategory = deps.parse_category_config(request.category_config)
|
||||||
|
cast = deps.parse_cast_config(request.cast_config)
|
||||||
|
profile = deps.parse_generation_profile(request.generation_profile)
|
||||||
|
filters = deps.parse_filter_config(request.filter_config)
|
||||||
|
build_kwargs: dict[str, Any] = {
|
||||||
|
"category": category,
|
||||||
|
"subcategory": subcategory,
|
||||||
|
"row_number": request.row_number,
|
||||||
|
"start_index": request.start_index,
|
||||||
|
"seed": request.seed,
|
||||||
|
"clothing": profile["clothing"],
|
||||||
|
"ethnicity": filters["ethnicity"],
|
||||||
|
"poses": profile["poses"],
|
||||||
|
"expression_enabled": profile["expression_enabled"],
|
||||||
|
"expression_intensity": profile["expression_intensity"],
|
||||||
|
"backside_bias": profile["backside_bias"],
|
||||||
|
"figure": filters["figure"],
|
||||||
|
"no_plus_women": filters["no_plus_women"],
|
||||||
|
"no_black": filters["no_black"],
|
||||||
|
"women_count": int(cast["women_count"]),
|
||||||
|
"men_count": int(cast["men_count"]),
|
||||||
|
"minimal_clothing_ratio": profile["minimal_clothing_ratio"],
|
||||||
|
"standard_pose_ratio": profile["standard_pose_ratio"],
|
||||||
|
"trigger": profile["trigger"],
|
||||||
|
"prepend_trigger_to_prompt": profile["prepend_trigger_to_prompt"],
|
||||||
|
"extra_positive": request.extra_positive or "",
|
||||||
|
"extra_negative": request.extra_negative or "",
|
||||||
|
"seed_config": request.seed_config or "",
|
||||||
|
"camera_config": request.camera_config or "",
|
||||||
|
"character_profile": request.character_profile or "",
|
||||||
|
"character_cast": request.character_cast or "",
|
||||||
|
"hardcore_position_config": request.hardcore_position_config or "",
|
||||||
|
"location_config": request.location_config or "",
|
||||||
|
"composition_config": request.composition_config or "",
|
||||||
|
}
|
||||||
|
return PromptFromConfigsRoute(
|
||||||
|
row=deps.build_prompt(**build_kwargs),
|
||||||
|
category=category,
|
||||||
|
subcategory=subcategory,
|
||||||
|
cast=dict(cast),
|
||||||
|
profile=dict(profile),
|
||||||
|
filters=dict(filters),
|
||||||
|
build_kwargs=build_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_prompt_from_configs(
|
||||||
|
request: PromptFromConfigsRequest,
|
||||||
|
deps: PromptFromConfigsDependencies,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return build_prompt_from_configs_result(request, deps).row
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import seed_config as seed_policy
|
||||||
|
except ImportError: # pragma: no cover - plain-script smoke tests
|
||||||
|
import seed_config as seed_policy
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PromptBuildRequest:
|
||||||
|
category: str
|
||||||
|
subcategory: str
|
||||||
|
row_number: int
|
||||||
|
start_index: int
|
||||||
|
seed: int
|
||||||
|
clothing: str
|
||||||
|
ethnicity: str
|
||||||
|
poses: str
|
||||||
|
backside_bias: float
|
||||||
|
figure: str
|
||||||
|
no_plus_women: bool
|
||||||
|
no_black: bool
|
||||||
|
minimal_clothing_ratio: float
|
||||||
|
standard_pose_ratio: float
|
||||||
|
trigger: str
|
||||||
|
prepend_trigger_to_prompt: bool
|
||||||
|
extra_positive: str
|
||||||
|
extra_negative: str
|
||||||
|
seed_config: str | dict[str, Any] | None = None
|
||||||
|
women_count: int = 1
|
||||||
|
men_count: int = 1
|
||||||
|
camera_config: str | dict[str, Any] | None = None
|
||||||
|
expression_intensity: float = 0.5
|
||||||
|
character_profile: str | dict[str, Any] | None = None
|
||||||
|
character_cast: str | dict[str, Any] | list[Any] | None = None
|
||||||
|
expression_enabled: bool = True
|
||||||
|
expression_phase: str = ""
|
||||||
|
hardcore_position_config: str | dict[str, Any] | None = None
|
||||||
|
location_config: str | dict[str, Any] | None = None
|
||||||
|
composition_config: str | dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PromptBuildRoute:
|
||||||
|
row: dict[str, Any]
|
||||||
|
category: str
|
||||||
|
subcategory: str
|
||||||
|
branch: str
|
||||||
|
parsed_seed_config: dict[str, Any]
|
||||||
|
expression_intensity: float
|
||||||
|
expression_intensity_source: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PromptBuildDependencies:
|
||||||
|
default_trigger: str
|
||||||
|
default_negative: str
|
||||||
|
random_subcategory: str
|
||||||
|
apply_pool_extensions: Callable[[], Any]
|
||||||
|
normalize_ethnicity_filter: Callable[[Any, str], str]
|
||||||
|
is_false: Callable[[Any], bool]
|
||||||
|
ratio_or_none: Callable[[Any], float | None]
|
||||||
|
parse_seed_config: Callable[[str | dict[str, Any] | None], dict[str, Any]]
|
||||||
|
parse_location_config: Callable[[str | dict[str, Any] | None], dict[str, Any]]
|
||||||
|
parse_composition_config: Callable[[str | dict[str, Any] | None], dict[str, Any]]
|
||||||
|
axis_rng: Callable[[dict[str, Any], str, int, int], Any]
|
||||||
|
pick_clothing_mode: Callable[[Any, str, float | None], str]
|
||||||
|
pick_pose_mode: Callable[[Any, str, float | None], str]
|
||||||
|
pick_figure_bias: Callable[[Any, str], str]
|
||||||
|
pick_expression_intensity: Callable[[Any, Any], tuple[float, str]]
|
||||||
|
auto_full_choice: Callable[[dict[str, Any], int, int], str]
|
||||||
|
build_auto_weighted_row: Callable[..., dict[str, Any]]
|
||||||
|
build_direct_builtin_row: Callable[..., dict[str, Any]]
|
||||||
|
build_custom_row: Callable[..., dict[str, Any]]
|
||||||
|
apply_location_config_to_legacy_row: Callable[..., dict[str, Any]]
|
||||||
|
apply_composition_config_to_legacy_row: Callable[..., dict[str, Any]]
|
||||||
|
disable_row_expression: Callable[[dict[str, Any], str], dict[str, Any]]
|
||||||
|
apply_camera_config: Callable[[dict[str, Any], str | dict[str, Any] | None], dict[str, Any]]
|
||||||
|
normalize_prompt_row: Callable[..., dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
def _generation_trace(
|
||||||
|
*,
|
||||||
|
row: dict[str, Any],
|
||||||
|
request: PromptBuildRequest,
|
||||||
|
row_number: int,
|
||||||
|
start_index: int,
|
||||||
|
seed: int,
|
||||||
|
category: str,
|
||||||
|
subcategory: str,
|
||||||
|
branch: str,
|
||||||
|
parsed_seed_config: dict[str, Any],
|
||||||
|
clothing: str,
|
||||||
|
poses: str,
|
||||||
|
figure: str,
|
||||||
|
expression_enabled: bool,
|
||||||
|
expression_intensity: float,
|
||||||
|
expression_intensity_source: str,
|
||||||
|
exact_custom_subcategory: bool,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
trace = {
|
||||||
|
"builder": "prompt_builder",
|
||||||
|
"branch": branch,
|
||||||
|
"source": row.get("source", ""),
|
||||||
|
"category_input": request.category,
|
||||||
|
"subcategory_input": request.subcategory,
|
||||||
|
"category": category,
|
||||||
|
"subcategory": row.get("subcategory") or subcategory,
|
||||||
|
"category_slug": row.get("category_slug", ""),
|
||||||
|
"subcategory_slug": row.get("subcategory_slug", ""),
|
||||||
|
"exact_custom_subcategory": bool(exact_custom_subcategory),
|
||||||
|
"row_number": row_number,
|
||||||
|
"start_index": start_index,
|
||||||
|
"seed": seed,
|
||||||
|
"seed_axes": seed_policy.axis_seed_trace(parsed_seed_config, seed, row_number),
|
||||||
|
"content_seed_axis": row.get("content_seed_axis") or ("pose" if row.get("position_family") else "content"),
|
||||||
|
"clothing": clothing,
|
||||||
|
"poses": poses,
|
||||||
|
"figure": figure,
|
||||||
|
"expression_enabled": bool(expression_enabled),
|
||||||
|
"expression_intensity": expression_intensity,
|
||||||
|
"expression_intensity_source": expression_intensity_source,
|
||||||
|
"trigger": row.get("trigger", ""),
|
||||||
|
}
|
||||||
|
if row.get("cast_count_adjustment"):
|
||||||
|
trace["cast_count_adjustment"] = row.get("cast_count_adjustment")
|
||||||
|
return trace
|
||||||
|
|
||||||
|
|
||||||
|
def build_prompt_result(request: PromptBuildRequest, deps: PromptBuildDependencies) -> PromptBuildRoute:
|
||||||
|
deps.apply_pool_extensions()
|
||||||
|
row_number = max(1, int(request.row_number))
|
||||||
|
start_index = max(1, int(request.start_index))
|
||||||
|
seed = int(request.seed)
|
||||||
|
category = request.category
|
||||||
|
subcategory = request.subcategory
|
||||||
|
ethnicity = deps.normalize_ethnicity_filter(request.ethnicity, "any")
|
||||||
|
expression_enabled = not deps.is_false(request.expression_enabled)
|
||||||
|
minimal_ratio = deps.ratio_or_none(request.minimal_clothing_ratio)
|
||||||
|
pose_ratio = deps.ratio_or_none(request.standard_pose_ratio)
|
||||||
|
parsed_seed_config = deps.parse_seed_config(request.seed_config)
|
||||||
|
parsed_location_config = deps.parse_location_config(request.location_config)
|
||||||
|
parsed_composition_config = deps.parse_composition_config(request.composition_config)
|
||||||
|
content_rng = deps.axis_rng(parsed_seed_config, "content", seed, row_number)
|
||||||
|
pose_axis_rng = deps.axis_rng(parsed_seed_config, "pose", seed, row_number)
|
||||||
|
person_rng = deps.axis_rng(parsed_seed_config, "person", seed, row_number)
|
||||||
|
expression_rng = deps.axis_rng(parsed_seed_config, "expression", seed, row_number)
|
||||||
|
clothing = request.clothing if request.clothing in ("full", "minimal", "random") else "full"
|
||||||
|
poses = request.poses if request.poses in ("standard", "evocative", "random") else "standard"
|
||||||
|
figure = request.figure if request.figure in ("curvy", "balanced", "bombshell", "random") else "curvy"
|
||||||
|
clothing = deps.pick_clothing_mode(content_rng, clothing, minimal_ratio)
|
||||||
|
poses = deps.pick_pose_mode(pose_axis_rng, poses, pose_ratio)
|
||||||
|
figure = deps.pick_figure_bias(person_rng, figure)
|
||||||
|
minimal_ratio = None
|
||||||
|
pose_ratio = None
|
||||||
|
expression_intensity, expression_intensity_source = deps.pick_expression_intensity(
|
||||||
|
expression_rng,
|
||||||
|
request.expression_intensity,
|
||||||
|
)
|
||||||
|
|
||||||
|
exact_custom_subcategory = bool(
|
||||||
|
subcategory and subcategory != deps.random_subcategory and " / " in subcategory
|
||||||
|
)
|
||||||
|
|
||||||
|
if category == "auto_full" and not exact_custom_subcategory:
|
||||||
|
category = deps.auto_full_choice(parsed_seed_config, seed, row_number)
|
||||||
|
|
||||||
|
branch = "custom"
|
||||||
|
if category == "auto_weighted" and not exact_custom_subcategory:
|
||||||
|
branch = "auto_weighted"
|
||||||
|
row = deps.build_auto_weighted_row(
|
||||||
|
row_number,
|
||||||
|
start_index,
|
||||||
|
clothing,
|
||||||
|
ethnicity,
|
||||||
|
poses,
|
||||||
|
float(request.backside_bias),
|
||||||
|
figure,
|
||||||
|
bool(request.no_plus_women),
|
||||||
|
bool(request.no_black),
|
||||||
|
minimal_ratio,
|
||||||
|
pose_ratio,
|
||||||
|
seed,
|
||||||
|
)
|
||||||
|
elif category in ("woman", "man", "couple", "group_or_layout") and not exact_custom_subcategory:
|
||||||
|
branch = "built_in"
|
||||||
|
row = deps.build_direct_builtin_row(
|
||||||
|
category,
|
||||||
|
row_number,
|
||||||
|
start_index,
|
||||||
|
clothing,
|
||||||
|
ethnicity,
|
||||||
|
poses,
|
||||||
|
float(request.backside_bias),
|
||||||
|
figure,
|
||||||
|
bool(request.no_plus_women),
|
||||||
|
bool(request.no_black),
|
||||||
|
minimal_ratio,
|
||||||
|
pose_ratio,
|
||||||
|
seed,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
row = deps.build_custom_row(
|
||||||
|
category,
|
||||||
|
subcategory,
|
||||||
|
row_number,
|
||||||
|
start_index,
|
||||||
|
ethnicity,
|
||||||
|
poses,
|
||||||
|
figure,
|
||||||
|
bool(request.no_plus_women),
|
||||||
|
bool(request.no_black),
|
||||||
|
int(request.women_count),
|
||||||
|
int(request.men_count),
|
||||||
|
seed,
|
||||||
|
parsed_seed_config,
|
||||||
|
expression_enabled,
|
||||||
|
expression_intensity,
|
||||||
|
expression_intensity_source,
|
||||||
|
request.character_profile,
|
||||||
|
request.character_cast,
|
||||||
|
request.expression_phase,
|
||||||
|
request.hardcore_position_config,
|
||||||
|
parsed_location_config,
|
||||||
|
parsed_composition_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
if row.get("source") == "built_in_generator":
|
||||||
|
row = deps.apply_location_config_to_legacy_row(
|
||||||
|
row,
|
||||||
|
parsed_location_config,
|
||||||
|
parsed_seed_config,
|
||||||
|
seed,
|
||||||
|
row_number,
|
||||||
|
)
|
||||||
|
row = deps.apply_composition_config_to_legacy_row(
|
||||||
|
row,
|
||||||
|
parsed_composition_config,
|
||||||
|
parsed_seed_config,
|
||||||
|
seed,
|
||||||
|
row_number,
|
||||||
|
)
|
||||||
|
if not expression_enabled:
|
||||||
|
row = deps.disable_row_expression(row, "disabled")
|
||||||
|
row = deps.apply_camera_config(row, request.camera_config)
|
||||||
|
active_trigger = request.trigger.strip() or deps.default_trigger
|
||||||
|
row = deps.normalize_prompt_row(
|
||||||
|
row,
|
||||||
|
active_trigger=active_trigger,
|
||||||
|
prepend_trigger_to_prompt=bool(request.prepend_trigger_to_prompt),
|
||||||
|
extra_positive=request.extra_positive,
|
||||||
|
extra_negative=request.extra_negative,
|
||||||
|
default_negative=deps.default_negative,
|
||||||
|
)
|
||||||
|
row.setdefault("expression_intensity", expression_intensity)
|
||||||
|
row.setdefault("expression_intensity_source", expression_intensity_source)
|
||||||
|
row["generation_trace"] = _generation_trace(
|
||||||
|
row=row,
|
||||||
|
request=request,
|
||||||
|
row_number=row_number,
|
||||||
|
start_index=start_index,
|
||||||
|
seed=seed,
|
||||||
|
category=category,
|
||||||
|
subcategory=subcategory,
|
||||||
|
branch=branch,
|
||||||
|
parsed_seed_config=parsed_seed_config,
|
||||||
|
clothing=clothing,
|
||||||
|
poses=poses,
|
||||||
|
figure=figure,
|
||||||
|
expression_enabled=expression_enabled,
|
||||||
|
expression_intensity=expression_intensity,
|
||||||
|
expression_intensity_source=expression_intensity_source,
|
||||||
|
exact_custom_subcategory=exact_custom_subcategory,
|
||||||
|
)
|
||||||
|
return PromptBuildRoute(
|
||||||
|
row=row,
|
||||||
|
category=category,
|
||||||
|
subcategory=subcategory,
|
||||||
|
branch=branch,
|
||||||
|
parsed_seed_config=dict(parsed_seed_config),
|
||||||
|
expression_intensity=expression_intensity,
|
||||||
|
expression_intensity_source=expression_intensity_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_prompt(request: PromptBuildRequest, deps: PromptBuildDependencies) -> dict[str, Any]:
|
||||||
|
return build_prompt_result(request, deps).row
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import formatter_input as input_policy
|
||||||
|
from . import formatter_route_trace as trace_policy
|
||||||
|
from . import formatter_target as target_policy
|
||||||
|
except ImportError: # pragma: no cover - plain-script smoke tests
|
||||||
|
import formatter_input as input_policy
|
||||||
|
import formatter_route_trace as trace_policy
|
||||||
|
import formatter_target as target_policy
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CaptionFormatRequest:
|
||||||
|
source_text: str
|
||||||
|
metadata_json: str = ""
|
||||||
|
input_hint: str = "auto"
|
||||||
|
target: str = "auto"
|
||||||
|
trigger: str = ""
|
||||||
|
include_trigger: bool = True
|
||||||
|
detail_level: str = "balanced"
|
||||||
|
style_policy: str = "drop_style_tail"
|
||||||
|
caption_profile: str = "manual_controls"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CaptionFormatRoute:
|
||||||
|
caption: str
|
||||||
|
method: str
|
||||||
|
branch: str
|
||||||
|
input_hint: str
|
||||||
|
target: str
|
||||||
|
detail_level: str
|
||||||
|
style_policy: str
|
||||||
|
include_trigger: bool
|
||||||
|
keep_style: bool
|
||||||
|
route_trace_json: str = ""
|
||||||
|
|
||||||
|
def as_tuple(self) -> tuple[str, str]:
|
||||||
|
return self.caption, self.method
|
||||||
|
|
||||||
|
def as_trace_tuple(self) -> tuple[str, str, str]:
|
||||||
|
return self.caption, self.method, self.route_trace_json
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CaptionFormatDependencies:
|
||||||
|
apply_caption_profile: Callable[[str, str, str, bool], tuple[str, str, bool]]
|
||||||
|
keep_style_terms: Callable[[str], bool]
|
||||||
|
row_from_inputs: Callable[[str, str, str], tuple[dict[str, Any] | None, str]]
|
||||||
|
metadata_to_prose: Callable[..., tuple[str, str]]
|
||||||
|
text_to_prose: Callable[[str, str, bool], tuple[str, str]]
|
||||||
|
with_trigger: Callable[[str, str, bool], str]
|
||||||
|
sanitize_prose_text: Callable[..., str]
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_caption_result(
|
||||||
|
request: CaptionFormatRequest,
|
||||||
|
deps: CaptionFormatDependencies,
|
||||||
|
) -> CaptionFormatRoute:
|
||||||
|
input_hint = input_policy.normalize_input_hint(request.input_hint, text_hint=input_policy.INPUT_HINT_CAPTION_OR_PROMPT)
|
||||||
|
target = target_policy.normalize_target(request.target)
|
||||||
|
detail_level, style_policy, include_trigger = deps.apply_caption_profile(
|
||||||
|
request.caption_profile,
|
||||||
|
request.detail_level,
|
||||||
|
request.style_policy,
|
||||||
|
request.include_trigger,
|
||||||
|
)
|
||||||
|
keep_style = deps.keep_style_terms(style_policy)
|
||||||
|
row, row_method = deps.row_from_inputs(request.source_text, request.metadata_json, input_hint)
|
||||||
|
if row is not None:
|
||||||
|
prose, method = deps.metadata_to_prose(row, detail_level, keep_style, target)
|
||||||
|
caption = deps.sanitize_prose_text(
|
||||||
|
deps.with_trigger(prose, request.trigger, include_trigger),
|
||||||
|
triggers=(request.trigger,),
|
||||||
|
)
|
||||||
|
full_method = f"{row_method}:{method}"
|
||||||
|
route_trace = trace_policy.route_trace_json(
|
||||||
|
formatter="caption",
|
||||||
|
branch="metadata",
|
||||||
|
method=full_method,
|
||||||
|
input_hint=input_hint,
|
||||||
|
target=target,
|
||||||
|
detail_level=detail_level,
|
||||||
|
style_policy=style_policy,
|
||||||
|
include_trigger=include_trigger,
|
||||||
|
keep_style=keep_style,
|
||||||
|
**trace_policy.metadata_trace_fields(row, target=target),
|
||||||
|
)
|
||||||
|
return CaptionFormatRoute(
|
||||||
|
caption=caption,
|
||||||
|
method=full_method,
|
||||||
|
branch="metadata",
|
||||||
|
input_hint=input_hint,
|
||||||
|
target=target,
|
||||||
|
detail_level=detail_level,
|
||||||
|
style_policy=style_policy,
|
||||||
|
include_trigger=include_trigger,
|
||||||
|
keep_style=keep_style,
|
||||||
|
route_trace_json=route_trace,
|
||||||
|
)
|
||||||
|
|
||||||
|
prose, method = deps.text_to_prose(request.source_text, detail_level, keep_style)
|
||||||
|
caption = deps.sanitize_prose_text(
|
||||||
|
deps.with_trigger(prose, request.trigger, include_trigger),
|
||||||
|
triggers=(request.trigger,),
|
||||||
|
)
|
||||||
|
route_trace = trace_policy.route_trace_json(
|
||||||
|
formatter="caption",
|
||||||
|
branch="text",
|
||||||
|
method=method,
|
||||||
|
input_hint=input_hint,
|
||||||
|
target=target,
|
||||||
|
detail_level=detail_level,
|
||||||
|
style_policy=style_policy,
|
||||||
|
include_trigger=include_trigger,
|
||||||
|
keep_style=keep_style,
|
||||||
|
)
|
||||||
|
return CaptionFormatRoute(
|
||||||
|
caption=caption,
|
||||||
|
method=method,
|
||||||
|
branch="text",
|
||||||
|
input_hint=input_hint,
|
||||||
|
target=target,
|
||||||
|
detail_level=detail_level,
|
||||||
|
style_policy=style_policy,
|
||||||
|
include_trigger=include_trigger,
|
||||||
|
keep_style=keep_style,
|
||||||
|
route_trace_json=route_trace,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_caption(
|
||||||
|
request: CaptionFormatRequest,
|
||||||
|
deps: CaptionFormatDependencies,
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
return naturalize_caption_result(request, deps).as_tuple()
|
||||||
+102
-24
@@ -4,12 +4,20 @@ import re
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import formatter_input as input_policy
|
||||||
|
from . import formatter_target as target_policy
|
||||||
|
except ImportError: # pragma: no cover - plain-script smoke tests
|
||||||
|
import formatter_input as input_policy
|
||||||
|
import formatter_target as target_policy
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CaptionMetadataRouteRequest:
|
class CaptionMetadataRouteRequest:
|
||||||
row: dict[str, Any]
|
row: dict[str, Any]
|
||||||
detail_level: str
|
detail_level: str
|
||||||
keep_style: bool
|
keep_style: bool
|
||||||
|
target: str = "auto"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -43,10 +51,12 @@ class CaptionMetadataRouteDependencies:
|
|||||||
subject_phrase_from_counts: Callable[[dict[str, Any]], str]
|
subject_phrase_from_counts: Callable[[dict[str, Any]], str]
|
||||||
verb_for_row: Callable[[dict[str, Any]], str]
|
verb_for_row: Callable[[dict[str, Any]], str]
|
||||||
metadata_action_label: Callable[[dict[str, Any]], str]
|
metadata_action_label: Callable[[dict[str, Any]], str]
|
||||||
|
item_axis_detail_text: Callable[[dict[str, Any], str], str]
|
||||||
natural_cast_descriptor_text: Callable[[str], str]
|
natural_cast_descriptor_text: Callable[[str], str]
|
||||||
cast_labels: Callable[[str], list[str]]
|
cast_labels: Callable[[str], list[str]]
|
||||||
natural_label_text: Callable[[Any, list[str]], str]
|
natural_label_text: Callable[[Any, list[str]], str]
|
||||||
metadata_to_prose: Callable[[dict[str, Any], str, bool], tuple[str, str]]
|
softcore_caption_setup_phrase: Callable[..., str]
|
||||||
|
metadata_to_prose: Callable[..., tuple[str, str]]
|
||||||
|
|
||||||
|
|
||||||
def pronoun(subject: str) -> str:
|
def pronoun(subject: str) -> str:
|
||||||
@@ -69,6 +79,41 @@ def couple_clothing_sentence(clothing: str, clean_text: Callable[[Any], str]) ->
|
|||||||
return f"They wear {clothing}"
|
return f"They wear {clothing}"
|
||||||
|
|
||||||
|
|
||||||
|
def couple_subject_sentence(
|
||||||
|
subject: str,
|
||||||
|
ages: str,
|
||||||
|
cap_first: Callable[[str], str],
|
||||||
|
clean_age_phrase: Callable[[str], str],
|
||||||
|
) -> str:
|
||||||
|
subject = cap_first(subject or "adult couple")
|
||||||
|
ages = clean_age_phrase(ages)
|
||||||
|
if ages:
|
||||||
|
return f"{subject}, {ages}"
|
||||||
|
if subject.lower() == "adult couple":
|
||||||
|
return subject
|
||||||
|
return f"{subject} are adults"
|
||||||
|
|
||||||
|
|
||||||
|
def expression_detail(expression: Any, clean_text: Callable[[Any], str]) -> tuple[str, bool]:
|
||||||
|
text = clean_text(expression)
|
||||||
|
if not text:
|
||||||
|
return "", False
|
||||||
|
has_character_labels = bool(
|
||||||
|
re.search(
|
||||||
|
r"\b(?:Woman|Man) [A-Z] has\b|\bthe (?:woman|man) has\b",
|
||||||
|
text,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text = re.sub(
|
||||||
|
r"\b((?:Woman|Man) [A-Z]|the (?:woman|man)) has\b",
|
||||||
|
r"\1 with",
|
||||||
|
text,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
return text, has_character_labels
|
||||||
|
|
||||||
|
|
||||||
def single_from_row_result(
|
def single_from_row_result(
|
||||||
request: CaptionMetadataRouteRequest,
|
request: CaptionMetadataRouteRequest,
|
||||||
deps: CaptionMetadataRouteDependencies,
|
deps: CaptionMetadataRouteDependencies,
|
||||||
@@ -123,7 +168,11 @@ def single_from_row_result(
|
|||||||
if pose:
|
if pose:
|
||||||
parts.append(f"{pronoun(subject)} is {deps.pose_clause(pose)}")
|
parts.append(f"{pronoun(subject)} is {deps.pose_clause(pose)}")
|
||||||
if expression:
|
if expression:
|
||||||
parts.append(f"{possessive_pronoun(subject)} expression is {expression}")
|
expression, labeled_expression = expression_detail(expression, deps.clean_text)
|
||||||
|
if labeled_expression:
|
||||||
|
parts.append(f"The expression detail shows {expression}")
|
||||||
|
else:
|
||||||
|
parts.append(f"{possessive_pronoun(subject)} expression is {expression}")
|
||||||
if scene:
|
if scene:
|
||||||
parts.append(f"The setting is {scene}")
|
parts.append(f"The setting is {scene}")
|
||||||
if deps.detail_allows(detail_level) and camera_scene:
|
if deps.detail_allows(detail_level) and camera_scene:
|
||||||
@@ -167,9 +216,7 @@ def couple_from_row_result(
|
|||||||
camera_scene = deps.clean_text(row.get("camera_scene_directive"))
|
camera_scene = deps.clean_text(row.get("camera_scene_directive"))
|
||||||
style = deps.field_row_value(row, "style") if keep_style else ""
|
style = deps.field_row_value(row, "style") if keep_style else ""
|
||||||
|
|
||||||
parts = [f"{deps.cap_first(subject)} are adults"]
|
parts = [couple_subject_sentence(subject, ages, deps.cap_first, deps.clean_age_phrase)]
|
||||||
if ages:
|
|
||||||
parts.append(f"The age detail is {deps.clean_age_phrase(ages)}")
|
|
||||||
if body:
|
if body:
|
||||||
parts.append(f"Their body types are {body}")
|
parts.append(f"Their body types are {body}")
|
||||||
if clothing:
|
if clothing:
|
||||||
@@ -181,7 +228,11 @@ def couple_from_row_result(
|
|||||||
if deps.detail_allows(detail_level) and camera_scene:
|
if deps.detail_allows(detail_level) and camera_scene:
|
||||||
parts.append(camera_scene)
|
parts.append(camera_scene)
|
||||||
if expression:
|
if expression:
|
||||||
parts.append(f"Their expressions are {expression}")
|
expression, labeled_expression = expression_detail(expression, deps.clean_text)
|
||||||
|
if labeled_expression:
|
||||||
|
parts.append(f"The expression details show {expression}")
|
||||||
|
else:
|
||||||
|
parts.append(f"Their expressions are {expression}")
|
||||||
if deps.detail_allows(detail_level) and composition:
|
if deps.detail_allows(detail_level) and composition:
|
||||||
parts.append(f"The composition is {composition}")
|
parts.append(f"The composition is {composition}")
|
||||||
if keep_style and style:
|
if keep_style and style:
|
||||||
@@ -205,6 +256,7 @@ def configured_cast_from_row_result(
|
|||||||
cast = deps.row_value(row, "cast_summary", ("Cast",))
|
cast = deps.row_value(row, "cast_summary", ("Cast",))
|
||||||
role_graph = deps.row_value(row, "role_graph", ("Role graph",))
|
role_graph = deps.row_value(row, "role_graph", ("Role graph",))
|
||||||
item = deps.row_value(row, "item", deps.item_labels)
|
item = deps.row_value(row, "item", deps.item_labels)
|
||||||
|
axis_detail = deps.item_axis_detail_text(row, " ".join(part for part in (role_graph, item) if part))
|
||||||
scene = deps.row_value(row, "scene_text", ("Setting", "Scene"))
|
scene = deps.row_value(row, "scene_text", ("Setting", "Scene"))
|
||||||
expression = ""
|
expression = ""
|
||||||
if not deps.expression_disabled(row):
|
if not deps.expression_disabled(row):
|
||||||
@@ -228,11 +280,17 @@ def configured_cast_from_row_result(
|
|||||||
parts.append(role_graph)
|
parts.append(role_graph)
|
||||||
if item:
|
if item:
|
||||||
parts.append(f"The {deps.metadata_action_label(row)} is {item}")
|
parts.append(f"The {deps.metadata_action_label(row)} is {item}")
|
||||||
|
if axis_detail:
|
||||||
|
parts.append(f"Selected action details include {axis_detail}")
|
||||||
scene_bits = []
|
scene_bits = []
|
||||||
if scene:
|
if scene:
|
||||||
scene_bits.append(f"set in {scene}")
|
scene_bits.append(f"set in {scene}")
|
||||||
if expression:
|
if expression:
|
||||||
scene_bits.append(f"with {expression}")
|
expression, labeled_expression = expression_detail(expression, deps.clean_text)
|
||||||
|
if labeled_expression:
|
||||||
|
scene_bits.append(f"showing {expression}")
|
||||||
|
else:
|
||||||
|
scene_bits.append(f"with {expression}")
|
||||||
if composition:
|
if composition:
|
||||||
scene_bits.append(f"framed as {composition}")
|
scene_bits.append(f"framed as {composition}")
|
||||||
if scene_bits and deps.detail_allows(detail_level):
|
if scene_bits and deps.detail_allows(detail_level):
|
||||||
@@ -273,7 +331,11 @@ def group_or_layout_from_row_result(
|
|||||||
if primary == "layout scene":
|
if primary == "layout scene":
|
||||||
parts = [f"{deps.cap_first(subject)} is arranged as an adults-only designed illustration layout"]
|
parts = [f"{deps.cap_first(subject)} is arranged as an adults-only designed illustration layout"]
|
||||||
if expression:
|
if expression:
|
||||||
parts.append(f"The featured expression is {expression}")
|
expression, labeled_expression = expression_detail(expression, deps.clean_text)
|
||||||
|
if labeled_expression:
|
||||||
|
parts.append(f"The featured expression details show {expression}")
|
||||||
|
else:
|
||||||
|
parts.append(f"The featured expression is {expression}")
|
||||||
else:
|
else:
|
||||||
parts = [f"{deps.cap_first(subject)} includes adults"]
|
parts = [f"{deps.cap_first(subject)} includes adults"]
|
||||||
if age:
|
if age:
|
||||||
@@ -281,7 +343,11 @@ def group_or_layout_from_row_result(
|
|||||||
if item:
|
if item:
|
||||||
parts.append(f"They wear {item}")
|
parts.append(f"They wear {item}")
|
||||||
if expression:
|
if expression:
|
||||||
parts.append(f"They show {expression}")
|
expression, labeled_expression = expression_detail(expression, deps.clean_text)
|
||||||
|
if labeled_expression:
|
||||||
|
parts.append(f"Their expressions show {expression}")
|
||||||
|
else:
|
||||||
|
parts.append(f"They show {expression}")
|
||||||
if scene:
|
if scene:
|
||||||
parts.append(f"The setting is {scene}")
|
parts.append(f"The setting is {scene}")
|
||||||
if deps.detail_allows(detail_level) and camera_scene:
|
if deps.detail_allows(detail_level) and camera_scene:
|
||||||
@@ -300,7 +366,9 @@ def insta_of_pair_from_row_result(
|
|||||||
row = request.row
|
row = request.row
|
||||||
detail_level = request.detail_level
|
detail_level = request.detail_level
|
||||||
keep_style = request.keep_style
|
keep_style = request.keep_style
|
||||||
if deps.clean_text(row.get("mode")).lower() != "insta/of":
|
pair_target = target_policy.pair_policy(request.target)
|
||||||
|
target = pair_target.pair_target
|
||||||
|
if not input_policy.is_pair_metadata(row):
|
||||||
return None
|
return None
|
||||||
soft_row = row.get("softcore_row")
|
soft_row = row.get("softcore_row")
|
||||||
hard_row = row.get("hardcore_row")
|
hard_row = row.get("hardcore_row")
|
||||||
@@ -310,13 +378,19 @@ def insta_of_pair_from_row_result(
|
|||||||
hard_row_for_text = dict(hard_row)
|
hard_row_for_text = dict(hard_row)
|
||||||
options = row.get("options")
|
options = row.get("options")
|
||||||
if isinstance(options, dict) and options.get("continuity") == "same_creator_same_room":
|
if isinstance(options, dict) and options.get("continuity") == "same_creator_same_room":
|
||||||
if soft_row.get("scene_text"):
|
if not hard_row_for_text.get("scene_text") and soft_row.get("scene_text"):
|
||||||
hard_row_for_text["scene_text"] = soft_row["scene_text"]
|
hard_row_for_text["scene_text"] = soft_row["scene_text"]
|
||||||
if soft_row.get("composition"):
|
if not hard_row_for_text.get("composition") and soft_row.get("composition"):
|
||||||
hard_row_for_text["composition"] = soft_row["composition"]
|
hard_row_for_text["composition"] = soft_row["composition"]
|
||||||
|
|
||||||
soft_text, _soft_method = deps.metadata_to_prose(soft_row, detail_level, keep_style)
|
include_soft = pair_target.include_softcore
|
||||||
hard_text, _hard_method = deps.metadata_to_prose(hard_row_for_text, detail_level, keep_style)
|
include_hard = pair_target.include_hardcore
|
||||||
|
soft_text = ""
|
||||||
|
hard_text = ""
|
||||||
|
if include_soft:
|
||||||
|
soft_text, _soft_method = deps.metadata_to_prose(soft_row, detail_level, keep_style, "single")
|
||||||
|
if include_hard:
|
||||||
|
hard_text, _hard_method = deps.metadata_to_prose(hard_row_for_text, detail_level, keep_style, "single")
|
||||||
descriptor = deps.clean_text(row.get("shared_descriptor"))
|
descriptor = deps.clean_text(row.get("shared_descriptor"))
|
||||||
options = row.get("options") if isinstance(row.get("options"), dict) else {}
|
options = row.get("options") if isinstance(row.get("options"), dict) else {}
|
||||||
cast_descriptors = row.get("shared_cast_descriptors")
|
cast_descriptors = row.get("shared_cast_descriptors")
|
||||||
@@ -329,14 +403,18 @@ def insta_of_pair_from_row_result(
|
|||||||
same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"
|
same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
if cast_descriptor_text and same_soft_cast:
|
if not soft_text and not hard_text:
|
||||||
parts.append(deps.natural_cast_descriptor_text(cast_descriptor_text))
|
if cast_descriptor_text:
|
||||||
elif descriptor:
|
parts.append(deps.natural_cast_descriptor_text(cast_descriptor_text))
|
||||||
parts.append(f"A {descriptor}")
|
elif descriptor:
|
||||||
if cast_descriptor_text and not same_soft_cast:
|
parts.append(f"A {descriptor}")
|
||||||
parts.append(deps.natural_cast_descriptor_text(cast_descriptor_text))
|
if same_soft_cast and include_soft:
|
||||||
if same_soft_cast:
|
parts.append(
|
||||||
parts.append("The softcore version keeps the same adult cast present together in a non-explicit teaser setup")
|
deps.softcore_caption_setup_phrase(
|
||||||
|
same_cast=True,
|
||||||
|
target_auto=target == "auto",
|
||||||
|
)
|
||||||
|
)
|
||||||
partner_styling = row.get("softcore_partner_styling")
|
partner_styling = row.get("softcore_partner_styling")
|
||||||
if isinstance(partner_styling, dict):
|
if isinstance(partner_styling, dict):
|
||||||
outfits = partner_styling.get("outfits")
|
outfits = partner_styling.get("outfits")
|
||||||
@@ -349,9 +427,9 @@ def insta_of_pair_from_row_result(
|
|||||||
if pose:
|
if pose:
|
||||||
parts.append(f"The shared softcore cast pose is {pose}")
|
parts.append(f"The shared softcore cast pose is {pose}")
|
||||||
if soft_text:
|
if soft_text:
|
||||||
parts.append(f"Softcore version: {soft_text}")
|
parts.append(f"Softcore side: {soft_text}" if target == "auto" else soft_text)
|
||||||
if hard_text:
|
if hard_text:
|
||||||
parts.append(f"Hardcore version: {hard_text}")
|
parts.append(f"Hardcore side: {hard_text}" if target == "auto" else hard_text)
|
||||||
if not parts:
|
if not parts:
|
||||||
return None
|
return None
|
||||||
return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(insta_of_pair)")
|
return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(insta_of_pair)")
|
||||||
|
|||||||
+106
-27
@@ -3,12 +3,14 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from . import caption_format_route
|
||||||
from . import caption_metadata_routes
|
from . import caption_metadata_routes
|
||||||
from . import caption_policy
|
from . import caption_policy
|
||||||
from . import caption_text_policy
|
from . import caption_text_policy
|
||||||
from . import formatter_input as input_policy
|
from . import formatter_input as input_policy
|
||||||
from .prompt_hygiene import sanitize_prose_text
|
from .prompt_hygiene import sanitize_prose_text
|
||||||
except ImportError: # Allows local smoke tests with `python -c`.
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
|
import caption_format_route
|
||||||
import caption_metadata_routes
|
import caption_metadata_routes
|
||||||
import caption_policy
|
import caption_policy
|
||||||
import caption_text_policy
|
import caption_text_policy
|
||||||
@@ -170,17 +172,24 @@ def _caption_metadata_route_request(
|
|||||||
row: dict[str, Any],
|
row: dict[str, Any],
|
||||||
detail_level: str,
|
detail_level: str,
|
||||||
keep_style: bool,
|
keep_style: bool,
|
||||||
|
target: str = "auto",
|
||||||
) -> caption_metadata_routes.CaptionMetadataRouteRequest:
|
) -> caption_metadata_routes.CaptionMetadataRouteRequest:
|
||||||
return caption_metadata_routes.CaptionMetadataRouteRequest(
|
return caption_metadata_routes.CaptionMetadataRouteRequest(
|
||||||
row=row,
|
row=row,
|
||||||
detail_level=detail_level,
|
detail_level=detail_level,
|
||||||
keep_style=keep_style,
|
keep_style=keep_style,
|
||||||
|
target=target,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _single_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None:
|
def _single_from_row(
|
||||||
|
row: dict[str, Any],
|
||||||
|
detail_level: str,
|
||||||
|
keep_style: bool,
|
||||||
|
target: str = "auto",
|
||||||
|
) -> tuple[str, str] | None:
|
||||||
return caption_metadata_routes.single_from_row(
|
return caption_metadata_routes.single_from_row(
|
||||||
_caption_metadata_route_request(row, detail_level, keep_style),
|
_caption_metadata_route_request(row, detail_level, keep_style, target),
|
||||||
_caption_metadata_route_dependencies(),
|
_caption_metadata_route_dependencies(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -197,35 +206,60 @@ def _couple_clothing_sentence(clothing: str) -> str:
|
|||||||
return caption_metadata_routes.couple_clothing_sentence(clothing, _clean_text)
|
return caption_metadata_routes.couple_clothing_sentence(clothing, _clean_text)
|
||||||
|
|
||||||
|
|
||||||
def _couple_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None:
|
def _couple_from_row(
|
||||||
|
row: dict[str, Any],
|
||||||
|
detail_level: str,
|
||||||
|
keep_style: bool,
|
||||||
|
target: str = "auto",
|
||||||
|
) -> tuple[str, str] | None:
|
||||||
return caption_metadata_routes.couple_from_row(
|
return caption_metadata_routes.couple_from_row(
|
||||||
_caption_metadata_route_request(row, detail_level, keep_style),
|
_caption_metadata_route_request(row, detail_level, keep_style, target),
|
||||||
_caption_metadata_route_dependencies(),
|
_caption_metadata_route_dependencies(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _configured_cast_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None:
|
def _configured_cast_from_row(
|
||||||
|
row: dict[str, Any],
|
||||||
|
detail_level: str,
|
||||||
|
keep_style: bool,
|
||||||
|
target: str = "auto",
|
||||||
|
) -> tuple[str, str] | None:
|
||||||
return caption_metadata_routes.configured_cast_from_row(
|
return caption_metadata_routes.configured_cast_from_row(
|
||||||
_caption_metadata_route_request(row, detail_level, keep_style),
|
_caption_metadata_route_request(row, detail_level, keep_style, target),
|
||||||
_caption_metadata_route_dependencies(),
|
_caption_metadata_route_dependencies(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _group_or_layout_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None:
|
def _group_or_layout_from_row(
|
||||||
|
row: dict[str, Any],
|
||||||
|
detail_level: str,
|
||||||
|
keep_style: bool,
|
||||||
|
target: str = "auto",
|
||||||
|
) -> tuple[str, str] | None:
|
||||||
return caption_metadata_routes.group_or_layout_from_row(
|
return caption_metadata_routes.group_or_layout_from_row(
|
||||||
_caption_metadata_route_request(row, detail_level, keep_style),
|
_caption_metadata_route_request(row, detail_level, keep_style, target),
|
||||||
_caption_metadata_route_dependencies(),
|
_caption_metadata_route_dependencies(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _insta_of_pair_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None:
|
def _insta_of_pair_from_row(
|
||||||
|
row: dict[str, Any],
|
||||||
|
detail_level: str,
|
||||||
|
keep_style: bool,
|
||||||
|
target: str = "auto",
|
||||||
|
) -> tuple[str, str] | None:
|
||||||
return caption_metadata_routes.insta_of_pair_from_row(
|
return caption_metadata_routes.insta_of_pair_from_row(
|
||||||
_caption_metadata_route_request(row, detail_level, keep_style),
|
_caption_metadata_route_request(row, detail_level, keep_style, target),
|
||||||
_caption_metadata_route_dependencies(),
|
_caption_metadata_route_dependencies(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _metadata_to_prose(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str]:
|
def _metadata_to_prose(
|
||||||
|
row: dict[str, Any],
|
||||||
|
detail_level: str,
|
||||||
|
keep_style: bool,
|
||||||
|
target: str = "auto",
|
||||||
|
) -> tuple[str, str]:
|
||||||
for builder in (
|
for builder in (
|
||||||
_insta_of_pair_from_row,
|
_insta_of_pair_from_row,
|
||||||
_configured_cast_from_row,
|
_configured_cast_from_row,
|
||||||
@@ -233,7 +267,7 @@ def _metadata_to_prose(row: dict[str, Any], detail_level: str, keep_style: bool)
|
|||||||
_couple_from_row,
|
_couple_from_row,
|
||||||
_group_or_layout_from_row,
|
_group_or_layout_from_row,
|
||||||
):
|
):
|
||||||
result = builder(row, detail_level, keep_style)
|
result = builder(row, detail_level, keep_style, target)
|
||||||
if result:
|
if result:
|
||||||
prose, method = result
|
prose, method = result
|
||||||
return _append_formatter_hints(prose, row), method
|
return _append_formatter_hints(prose, row), method
|
||||||
@@ -318,6 +352,23 @@ def _text_to_prose(text: str, detail_level: str, keep_style: bool) -> tuple[str,
|
|||||||
return prose or _sentence(text), "text(fallback)"
|
return prose or _sentence(text), "text(fallback)"
|
||||||
|
|
||||||
|
|
||||||
|
def _caption_format_dependencies() -> caption_format_route.CaptionFormatDependencies:
|
||||||
|
return caption_format_route.CaptionFormatDependencies(
|
||||||
|
apply_caption_profile=lambda profile, detail, style, include: caption_policy.apply_caption_profile(
|
||||||
|
profile,
|
||||||
|
detail_level=detail,
|
||||||
|
style_policy=style,
|
||||||
|
include_trigger=include,
|
||||||
|
),
|
||||||
|
keep_style_terms=caption_policy.keep_style_terms,
|
||||||
|
row_from_inputs=_row_from_inputs,
|
||||||
|
metadata_to_prose=_metadata_to_prose,
|
||||||
|
text_to_prose=_text_to_prose,
|
||||||
|
with_trigger=_with_trigger,
|
||||||
|
sanitize_prose_text=sanitize_prose_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def naturalize_caption(
|
def naturalize_caption(
|
||||||
source_text: str,
|
source_text: str,
|
||||||
metadata_json: str = "",
|
metadata_json: str = "",
|
||||||
@@ -327,21 +378,49 @@ def naturalize_caption(
|
|||||||
detail_level: str = "balanced",
|
detail_level: str = "balanced",
|
||||||
style_policy: str = "drop_style_tail",
|
style_policy: str = "drop_style_tail",
|
||||||
caption_profile: str = caption_policy.CAPTION_PROFILE_DEFAULT,
|
caption_profile: str = caption_policy.CAPTION_PROFILE_DEFAULT,
|
||||||
|
target: str = "auto",
|
||||||
) -> tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
"""Rewrite tag-style prompt/caption text into compact natural language."""
|
"""Rewrite tag-style prompt/caption text into compact natural language."""
|
||||||
input_hint = input_hint if input_hint in ("auto", "metadata_json", "caption_or_prompt") else "auto"
|
return caption_format_route.naturalize_caption(
|
||||||
detail_level, style_policy, include_trigger = caption_policy.apply_caption_profile(
|
caption_format_route.CaptionFormatRequest(
|
||||||
caption_profile,
|
source_text=source_text,
|
||||||
detail_level=detail_level,
|
metadata_json=metadata_json,
|
||||||
style_policy=style_policy,
|
input_hint=input_hint,
|
||||||
include_trigger=include_trigger,
|
target=target,
|
||||||
|
trigger=trigger,
|
||||||
|
include_trigger=include_trigger,
|
||||||
|
detail_level=detail_level,
|
||||||
|
style_policy=style_policy,
|
||||||
|
caption_profile=caption_profile,
|
||||||
|
),
|
||||||
|
_caption_format_dependencies(),
|
||||||
)
|
)
|
||||||
keep_style = caption_policy.keep_style_terms(style_policy)
|
|
||||||
row, row_method = _row_from_inputs(source_text, metadata_json, input_hint)
|
|
||||||
if row is not None:
|
def naturalize_caption_with_trace(
|
||||||
prose, method = _metadata_to_prose(row, detail_level, keep_style)
|
source_text: str,
|
||||||
caption = sanitize_prose_text(_with_trigger(prose, trigger, include_trigger), triggers=(trigger,))
|
metadata_json: str = "",
|
||||||
return caption, f"{row_method}:{method}"
|
input_hint: str = "auto",
|
||||||
prose, method = _text_to_prose(source_text, detail_level, keep_style)
|
target: str = "auto",
|
||||||
caption = sanitize_prose_text(_with_trigger(prose, trigger, include_trigger), triggers=(trigger,))
|
trigger: str = DEFAULT_TRIGGER,
|
||||||
return caption, method
|
include_trigger: bool = True,
|
||||||
|
detail_level: str = "balanced",
|
||||||
|
style_policy: str = "drop_style_tail",
|
||||||
|
caption_profile: str = caption_policy.CAPTION_PROFILE_DEFAULT,
|
||||||
|
) -> tuple[str, str, str]:
|
||||||
|
"""Rewrite text like naturalize_caption and include formatter route trace JSON."""
|
||||||
|
result = caption_format_route.naturalize_caption_result(
|
||||||
|
caption_format_route.CaptionFormatRequest(
|
||||||
|
source_text=source_text,
|
||||||
|
metadata_json=metadata_json,
|
||||||
|
input_hint=input_hint,
|
||||||
|
target=target,
|
||||||
|
trigger=trigger,
|
||||||
|
include_trigger=include_trigger,
|
||||||
|
detail_level=detail_level,
|
||||||
|
style_policy=style_policy,
|
||||||
|
caption_profile=caption_profile,
|
||||||
|
),
|
||||||
|
_caption_format_dependencies(),
|
||||||
|
)
|
||||||
|
return result.as_trace_tuple()
|
||||||
|
|||||||
+23
-7
@@ -4,9 +4,11 @@ import re
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from . import formatter_detail as detail_policy
|
||||||
from . import formatter_input as input_policy
|
from . import formatter_input as input_policy
|
||||||
from . import route_metadata as route_metadata_policy
|
from . import route_metadata as route_metadata_policy
|
||||||
except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.py`.
|
except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.py`.
|
||||||
|
import formatter_detail as detail_policy
|
||||||
import formatter_input as input_policy
|
import formatter_input as input_policy
|
||||||
import route_metadata as route_metadata_policy
|
import route_metadata as route_metadata_policy
|
||||||
|
|
||||||
@@ -14,7 +16,7 @@ except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.
|
|||||||
OLD_TRIGGER = "sxcpinup_coloredpencil"
|
OLD_TRIGGER = "sxcpinup_coloredpencil"
|
||||||
DEFAULT_TRIGGER = "sxcppnl7"
|
DEFAULT_TRIGGER = "sxcppnl7"
|
||||||
|
|
||||||
DETAIL_LEVELS = ("balanced", "concise", "dense")
|
DETAIL_LEVELS = detail_policy.DETAIL_LEVELS
|
||||||
STYLE_POLICIES = ("drop_style_tail", "keep_style_terms")
|
STYLE_POLICIES = ("drop_style_tail", "keep_style_terms")
|
||||||
CAPTION_PROFILE_DEFAULT = "manual_controls"
|
CAPTION_PROFILE_DEFAULT = "manual_controls"
|
||||||
|
|
||||||
@@ -49,10 +51,14 @@ ITEM_LABELS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
ACTION_FAMILY_CAPTION_LABELS = {
|
ACTION_FAMILY_CAPTION_LABELS = {
|
||||||
|
"anal": "anal action",
|
||||||
"foreplay": "foreplay action",
|
"foreplay": "foreplay action",
|
||||||
|
"manual": "manual action",
|
||||||
"outercourse": "non-penetrative action",
|
"outercourse": "non-penetrative action",
|
||||||
"oral": "oral action",
|
"oral": "oral action",
|
||||||
"penetration": "penetrative action",
|
"penetration": "penetrative action",
|
||||||
|
"threesome": "three-person action",
|
||||||
|
"group": "group action",
|
||||||
"toy_double": "toy-assisted double-contact action",
|
"toy_double": "toy-assisted double-contact action",
|
||||||
"climax": "climax action",
|
"climax": "climax action",
|
||||||
}
|
}
|
||||||
@@ -72,18 +78,28 @@ POSITION_FAMILY_CAPTION_LABELS = {
|
|||||||
|
|
||||||
|
|
||||||
def normalize_detail_level(value: str) -> str:
|
def normalize_detail_level(value: str) -> str:
|
||||||
return value if value in DETAIL_LEVELS else "balanced"
|
return detail_policy.normalize_detail_level(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _choice_key(value: Any) -> str:
|
||||||
|
return str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
||||||
|
|
||||||
|
|
||||||
def normalize_style_policy(value: str) -> str:
|
def normalize_style_policy(value: str) -> str:
|
||||||
|
value = _choice_key(value)
|
||||||
return value if value in STYLE_POLICIES else "drop_style_tail"
|
return value if value in STYLE_POLICIES else "drop_style_tail"
|
||||||
|
|
||||||
|
|
||||||
|
def style_policy_choices() -> list[str]:
|
||||||
|
return list(STYLE_POLICIES)
|
||||||
|
|
||||||
|
|
||||||
def caption_profile_choices() -> list[str]:
|
def caption_profile_choices() -> list[str]:
|
||||||
return list(CAPTION_PROFILES)
|
return list(CAPTION_PROFILES)
|
||||||
|
|
||||||
|
|
||||||
def normalize_caption_profile(value: str) -> str:
|
def normalize_caption_profile(value: str) -> str:
|
||||||
|
value = _choice_key(value)
|
||||||
return value if value in CAPTION_PROFILES else CAPTION_PROFILE_DEFAULT
|
return value if value in CAPTION_PROFILES else CAPTION_PROFILE_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
@@ -107,10 +123,7 @@ def keep_style_terms(style_policy: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def detail_allows(level: str, dense_only: bool = False) -> bool:
|
def detail_allows(level: str, dense_only: bool = False) -> bool:
|
||||||
level = normalize_detail_level((level or "balanced").strip().lower())
|
return detail_policy.detail_allows(level, dense_only=dense_only)
|
||||||
if dense_only:
|
|
||||||
return level == "dense"
|
|
||||||
return level != "concise"
|
|
||||||
|
|
||||||
|
|
||||||
def strip_style_tail(text: str) -> str:
|
def strip_style_tail(text: str) -> str:
|
||||||
@@ -132,7 +145,10 @@ def metadata_action_label(row: dict[str, Any], default: str = "sexual pose") ->
|
|||||||
|
|
||||||
|
|
||||||
def normalize_composition(text: str) -> str:
|
def normalize_composition(text: str) -> str:
|
||||||
return re.sub(r"^vertical\s+", "", input_policy.clean_text(text), flags=re.IGNORECASE)
|
text = re.sub(r"^vertical\s+", "", input_policy.clean_text(text), flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r"\s+composition$", "", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r"\bcomposition\b", "frame", text, flags=re.IGNORECASE)
|
||||||
|
return text.strip(" ,")
|
||||||
|
|
||||||
|
|
||||||
def clean_clothing(text: str) -> str:
|
def clean_clothing(text: str) -> str:
|
||||||
|
|||||||
+16
-1
@@ -7,14 +7,18 @@ try:
|
|||||||
from . import caption_metadata_routes
|
from . import caption_metadata_routes
|
||||||
from . import caption_policy
|
from . import caption_policy
|
||||||
from . import formatter_input as input_policy
|
from . import formatter_input as input_policy
|
||||||
|
from . import item_axis_policy
|
||||||
from . import krea_cast as cast_policy
|
from . import krea_cast as cast_policy
|
||||||
from . import route_metadata as route_metadata_policy
|
from . import route_metadata as route_metadata_policy
|
||||||
|
from . import softcore_text_policy
|
||||||
except ImportError: # Allows local smoke tests with `python -c`.
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
import caption_metadata_routes
|
import caption_metadata_routes
|
||||||
import caption_policy
|
import caption_policy
|
||||||
import formatter_input as input_policy
|
import formatter_input as input_policy
|
||||||
|
import item_axis_policy
|
||||||
import krea_cast as cast_policy
|
import krea_cast as cast_policy
|
||||||
import route_metadata as route_metadata_policy
|
import route_metadata as route_metadata_policy
|
||||||
|
import softcore_text_policy
|
||||||
|
|
||||||
|
|
||||||
OLD_TRIGGER = caption_policy.OLD_TRIGGER
|
OLD_TRIGGER = caption_policy.OLD_TRIGGER
|
||||||
@@ -95,6 +99,15 @@ def metadata_action_label(row: dict[str, Any], default: str = "sexual pose") ->
|
|||||||
return caption_policy.metadata_action_label(row, default)
|
return caption_policy.metadata_action_label(row, default)
|
||||||
|
|
||||||
|
|
||||||
|
def item_axis_detail_text(row: dict[str, Any], existing_text: str = "") -> str:
|
||||||
|
details = item_axis_policy.row_axis_value_texts(
|
||||||
|
row,
|
||||||
|
skip_keys=item_axis_policy.METADATA_AXIS_KEYS,
|
||||||
|
existing_text=existing_text,
|
||||||
|
)
|
||||||
|
return human_join(details)
|
||||||
|
|
||||||
|
|
||||||
def prompt_cast_descriptors(text: str) -> str:
|
def prompt_cast_descriptors(text: str) -> str:
|
||||||
return cast_policy.prompt_cast_descriptors(text)
|
return cast_policy.prompt_cast_descriptors(text)
|
||||||
|
|
||||||
@@ -274,7 +287,7 @@ def detail_allows(level: str, dense_only: bool = False) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def metadata_route_dependencies(
|
def metadata_route_dependencies(
|
||||||
metadata_to_prose: Callable[[dict[str, Any], str, bool], tuple[str, str]],
|
metadata_to_prose: Callable[..., tuple[str, str]],
|
||||||
) -> caption_metadata_routes.CaptionMetadataRouteDependencies:
|
) -> caption_metadata_routes.CaptionMetadataRouteDependencies:
|
||||||
return caption_metadata_routes.CaptionMetadataRouteDependencies(
|
return caption_metadata_routes.CaptionMetadataRouteDependencies(
|
||||||
item_labels=ITEM_LABELS,
|
item_labels=ITEM_LABELS,
|
||||||
@@ -297,8 +310,10 @@ def metadata_route_dependencies(
|
|||||||
subject_phrase_from_counts=subject_phrase_from_counts,
|
subject_phrase_from_counts=subject_phrase_from_counts,
|
||||||
verb_for_row=verb_for_row,
|
verb_for_row=verb_for_row,
|
||||||
metadata_action_label=metadata_action_label,
|
metadata_action_label=metadata_action_label,
|
||||||
|
item_axis_detail_text=item_axis_detail_text,
|
||||||
natural_cast_descriptor_text=natural_cast_descriptor_text,
|
natural_cast_descriptor_text=natural_cast_descriptor_text,
|
||||||
cast_labels=cast_labels,
|
cast_labels=cast_labels,
|
||||||
natural_label_text=natural_label_text,
|
natural_label_text=natural_label_text,
|
||||||
|
softcore_caption_setup_phrase=softcore_text_policy.softcore_caption_setup_phrase,
|
||||||
metadata_to_prose=metadata_to_prose,
|
metadata_to_prose=metadata_to_prose,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -92,6 +92,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 0.75,
|
"weight": 0.75,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "foreplay",
|
||||||
|
"position_family": "foreplay"
|
||||||
|
},
|
||||||
"item_label": "Foreplay action",
|
"item_label": "Foreplay action",
|
||||||
"positive_suffix": "Use clear adult body contact, readable hands and faces, visible undressing, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
"positive_suffix": "Use clear adult body contact, readable hands and faces, visible undressing, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Foreplay action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult body contact, kissing, caressing, undressing, visible arousal, exposed skin, and readable hand placement. {positive_suffix} Avoid: {negative_prompt}.",
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Foreplay action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult body contact, kissing, caressing, undressing, visible arousal, exposed skin, and readable hand placement. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
@@ -102,7 +106,7 @@
|
|||||||
"item_templates": [
|
"item_templates": [
|
||||||
"{tease_act} in {position}, with {touch_detail}, {clothing_detail}, and {mood_detail}",
|
"{tease_act} in {position}, with {touch_detail}, {clothing_detail}, and {mood_detail}",
|
||||||
"{position} featuring {tease_act}, {body_contact}, {touch_detail}, and {visibility}",
|
"{position} featuring {tease_act}, {body_contact}, {touch_detail}, and {visibility}",
|
||||||
"hardcore foreplay setup: {tease_act}, {clothing_detail}, {face_detail}, and {body_contact}",
|
"hardcore foreplay setup with {tease_act}, {clothing_detail}, {face_detail}, and {body_contact}",
|
||||||
"{tease_act} on {surface}, with {touch_detail}, {mood_detail}, and {visibility}",
|
"{tease_act} on {surface}, with {touch_detail}, {mood_detail}, and {visibility}",
|
||||||
"{position} while {tease_act}, with {face_detail}, {clothing_detail}, and {touch_detail}"
|
"{position} while {tease_act}, with {face_detail}, {clothing_detail}, and {touch_detail}"
|
||||||
],
|
],
|
||||||
@@ -211,6 +215,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 0.85,
|
"weight": 0.85,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "manual",
|
||||||
|
"position_family": "manual"
|
||||||
|
},
|
||||||
"item_label": "Manual action",
|
"item_label": "Manual action",
|
||||||
"positive_suffix": "Use clear adult manual contact, readable hands, explicit body positioning, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
"positive_suffix": "Use clear adult manual contact, readable hands, explicit body positioning, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Manual action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult manual stimulation, visible hands, exposed skin, clear body positioning, and readable reaction. {positive_suffix} Avoid: {negative_prompt}.",
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Manual action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult manual stimulation, visible hands, exposed skin, clear body positioning, and readable reaction. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
@@ -222,7 +230,7 @@
|
|||||||
"{manual_act} in {position}, with {manual_detail}, {body_contact}, and {visibility}",
|
"{manual_act} in {position}, with {manual_detail}, {body_contact}, and {visibility}",
|
||||||
"{position} featuring {manual_act}, {hand_detail}, {reaction_detail}, and {visibility}",
|
"{position} featuring {manual_act}, {hand_detail}, {reaction_detail}, and {visibility}",
|
||||||
"{manual_act} on {surface}, with {manual_detail}, {hand_detail}, and {reaction_detail}",
|
"{manual_act} on {surface}, with {manual_detail}, {hand_detail}, and {reaction_detail}",
|
||||||
"manual stimulation setup: {position}, {manual_act}, {body_contact}, and {visibility}",
|
"manual stimulation setup with {position}, {manual_act}, {body_contact}, and {visibility}",
|
||||||
"{position} while {manual_act}, with {hand_detail}, {manual_detail}, and {reaction_detail}"
|
"{position} while {manual_act}, with {hand_detail}, {manual_detail}, and {reaction_detail}"
|
||||||
],
|
],
|
||||||
"item_axes": {
|
"item_axes": {
|
||||||
@@ -316,6 +324,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 0.7,
|
"weight": 0.7,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "foreplay",
|
||||||
|
"position_family": "interaction"
|
||||||
|
},
|
||||||
"item_label": "Body interaction",
|
"item_label": "Body interaction",
|
||||||
"positive_suffix": "Use readable adult body contact, hands and mouth on skin, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
"positive_suffix": "Use readable adult body contact, hands and mouth on skin, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Body interaction: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult body worship, close skin contact, mouth and hand placement, exposed skin, and readable body positioning. {positive_suffix} Avoid: {negative_prompt}.",
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Body interaction: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult body worship, close skin contact, mouth and hand placement, exposed skin, and readable body positioning. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
@@ -327,7 +339,7 @@
|
|||||||
"{worship_act} in {position}, with {touch_detail}, {face_detail}, and {visibility}",
|
"{worship_act} in {position}, with {touch_detail}, {face_detail}, and {visibility}",
|
||||||
"{position} featuring {worship_act}, {body_contact}, {touch_detail}, and {reaction_detail}",
|
"{position} featuring {worship_act}, {body_contact}, {touch_detail}, and {reaction_detail}",
|
||||||
"{worship_act} on {surface}, with {body_contact}, {face_detail}, and {visibility}",
|
"{worship_act} on {surface}, with {body_contact}, {face_detail}, and {visibility}",
|
||||||
"body worship setup: {position}, {worship_act}, {touch_detail}, and {reaction_detail}",
|
"body worship setup with {position}, {worship_act}, {touch_detail}, and {reaction_detail}",
|
||||||
"{position} while {worship_act}, with {body_contact}, {face_detail}, and {visibility}"
|
"{position} while {worship_act}, with {body_contact}, {face_detail}, and {visibility}"
|
||||||
],
|
],
|
||||||
"item_axes": {
|
"item_axes": {
|
||||||
@@ -425,6 +437,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 0.65,
|
"weight": 0.65,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "foreplay",
|
||||||
|
"position_family": "interaction"
|
||||||
|
},
|
||||||
"item_label": "Transition action",
|
"item_label": "Transition action",
|
||||||
"positive_suffix": "Use readable adult movement, clothing being moved, hands guiding bodies, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
"positive_suffix": "Use readable adult movement, clothing being moved, hands guiding bodies, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Transition action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult undressing, position changes, visible hands, exposed skin, and clear movement from one sexual beat to the next. {positive_suffix} Avoid: {negative_prompt}.",
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Transition action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult undressing, position changes, visible hands, exposed skin, and clear movement from one sexual beat to the next. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
@@ -436,7 +452,7 @@
|
|||||||
"{transition_act} in {position}, with {clothing_detail}, {hand_detail}, and {visibility}",
|
"{transition_act} in {position}, with {clothing_detail}, {hand_detail}, and {visibility}",
|
||||||
"{position} featuring {transition_act}, {body_contact}, {clothing_detail}, and {movement_detail}",
|
"{position} featuring {transition_act}, {body_contact}, {clothing_detail}, and {movement_detail}",
|
||||||
"{transition_act} on {surface}, with {hand_detail}, {body_contact}, and {visibility}",
|
"{transition_act} on {surface}, with {hand_detail}, {body_contact}, and {visibility}",
|
||||||
"position transition: {position}, {transition_act}, {movement_detail}, and {clothing_detail}",
|
"position transition with {position}, {transition_act}, {movement_detail}, and {clothing_detail}",
|
||||||
"{position} while {transition_act}, with {hand_detail}, {body_contact}, and {visibility}"
|
"{position} while {transition_act}, with {hand_detail}, {body_contact}, and {visibility}"
|
||||||
],
|
],
|
||||||
"item_axes": {
|
"item_axes": {
|
||||||
@@ -530,6 +546,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 0.55,
|
"weight": 0.55,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "foreplay",
|
||||||
|
"position_family": "interaction"
|
||||||
|
},
|
||||||
"item_label": "Guidance action",
|
"item_label": "Guidance action",
|
||||||
"positive_suffix": "Use consensual adult control, readable hand placement, clear body positioning, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
"positive_suffix": "Use consensual adult control, readable hand placement, clear body positioning, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Guidance action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through consensual adult guidance, hair or wrist control, body positioning, visible hands, exposed skin, and clear power dynamic. {positive_suffix} Avoid: {negative_prompt}.",
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Guidance action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through consensual adult guidance, hair or wrist control, body positioning, visible hands, exposed skin, and clear power dynamic. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
@@ -541,7 +561,7 @@
|
|||||||
"{control_act} in {position}, with {hand_detail}, {body_contact}, and {visibility}",
|
"{control_act} in {position}, with {hand_detail}, {body_contact}, and {visibility}",
|
||||||
"{position} featuring {control_act}, {power_detail}, {hand_detail}, and {reaction_detail}",
|
"{position} featuring {control_act}, {power_detail}, {hand_detail}, and {reaction_detail}",
|
||||||
"{control_act} on {surface}, with {body_contact}, {power_detail}, and {visibility}",
|
"{control_act} on {surface}, with {body_contact}, {power_detail}, and {visibility}",
|
||||||
"consensual control setup: {position}, {control_act}, {hand_detail}, and {reaction_detail}",
|
"consensual control setup with {position}, {control_act}, {hand_detail}, and {reaction_detail}",
|
||||||
"{position} while {control_act}, with {power_detail}, {body_contact}, and {visibility}"
|
"{position} while {control_act}, with {power_detail}, {body_contact}, and {visibility}"
|
||||||
],
|
],
|
||||||
"item_axes": {
|
"item_axes": {
|
||||||
@@ -640,6 +660,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 0.6,
|
"weight": 0.6,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "foreplay",
|
||||||
|
"position_family": "interaction"
|
||||||
|
},
|
||||||
"item_label": "Camera performance",
|
"item_label": "Camera performance",
|
||||||
"positive_suffix": "Use creator-shot adult presentation, readable camera-facing pose, exposed skin, clear hand placement, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
"positive_suffix": "Use creator-shot adult presentation, readable camera-facing pose, exposed skin, clear hand placement, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Camera performance: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through camera-aware adult presentation, body opened or displayed to the viewer, visible hands, exposed skin, and clean creator-shot framing. {positive_suffix} Avoid: {negative_prompt}.",
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Camera performance: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through camera-aware adult presentation, body opened or displayed to the viewer, visible hands, exposed skin, and clean creator-shot framing. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
@@ -651,7 +675,7 @@
|
|||||||
"{performance_act} in {position}, with {presentation_detail}, {hand_detail}, and {visibility}",
|
"{performance_act} in {position}, with {presentation_detail}, {hand_detail}, and {visibility}",
|
||||||
"{position} featuring {performance_act}, {camera_detail}, {presentation_detail}, and {reaction_detail}",
|
"{position} featuring {performance_act}, {camera_detail}, {presentation_detail}, and {reaction_detail}",
|
||||||
"{performance_act} on {surface}, with {hand_detail}, {camera_detail}, and {visibility}",
|
"{performance_act} on {surface}, with {hand_detail}, {camera_detail}, and {visibility}",
|
||||||
"creator-performance setup: {position}, {performance_act}, {presentation_detail}, and {reaction_detail}",
|
"creator-performance setup with {position}, {performance_act}, {presentation_detail}, and {reaction_detail}",
|
||||||
"{position} while {performance_act}, with {camera_detail}, {hand_detail}, and {visibility}"
|
"{position} while {performance_act}, with {camera_detail}, {hand_detail}, and {visibility}"
|
||||||
],
|
],
|
||||||
"item_axes": {
|
"item_axes": {
|
||||||
@@ -744,6 +768,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 0.55,
|
"weight": 0.55,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "foreplay",
|
||||||
|
"position_family": "interaction"
|
||||||
|
},
|
||||||
"item_label": "Group interaction",
|
"item_label": "Group interaction",
|
||||||
"positive_suffix": "Use readable adult group coordination, clear body spacing, visible watching/touching roles, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
"positive_suffix": "Use readable adult group coordination, clear body spacing, visible watching/touching roles, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Group interaction: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult group coordination, watching, guiding hands, body presentation, exposed skin, and clear role spacing. {positive_suffix} Avoid: {negative_prompt}.",
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Group interaction: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult group coordination, watching, guiding hands, body presentation, exposed skin, and clear role spacing. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
@@ -755,7 +783,7 @@
|
|||||||
"{coordination_act} with {arrangement}, {touch_detail}, {reaction_detail}, and {visibility}",
|
"{coordination_act} with {arrangement}, {touch_detail}, {reaction_detail}, and {visibility}",
|
||||||
"{arrangement} featuring {coordination_act}, {body_contact}, {watching_detail}, and {visibility}",
|
"{arrangement} featuring {coordination_act}, {body_contact}, {watching_detail}, and {visibility}",
|
||||||
"{coordination_act} on {surface}, with {touch_detail}, {watching_detail}, and {body_contact}",
|
"{coordination_act} on {surface}, with {touch_detail}, {watching_detail}, and {body_contact}",
|
||||||
"group coordination setup: {arrangement}, {coordination_act}, {watching_detail}, and {visibility}",
|
"group coordination setup with {arrangement}, {coordination_act}, {watching_detail}, and {visibility}",
|
||||||
"{arrangement} while {coordination_act}, with {touch_detail}, {reaction_detail}, and {visibility}"
|
"{arrangement} while {coordination_act}, with {touch_detail}, {reaction_detail}, and {visibility}"
|
||||||
],
|
],
|
||||||
"item_axes": {
|
"item_axes": {
|
||||||
@@ -846,6 +874,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 0.35,
|
"weight": 0.35,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "foreplay",
|
||||||
|
"position_family": "interaction"
|
||||||
|
},
|
||||||
"item_label": "Aftermath interaction",
|
"item_label": "Aftermath interaction",
|
||||||
"positive_suffix": "Use adult post-sex intimacy, readable bodies and hands, visible aftermath details, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
"positive_suffix": "Use adult post-sex intimacy, readable bodies and hands, visible aftermath details, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Aftermath interaction: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult post-sex closeness, cleanup, visible skin, relaxed body contact, aftermath details, and readable hands and faces. {positive_suffix} Avoid: {negative_prompt}.",
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Aftermath interaction: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult post-sex closeness, cleanup, visible skin, relaxed body contact, aftermath details, and readable hands and faces. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
@@ -857,7 +889,7 @@
|
|||||||
"{aftercare_act} in {position}, with {cleanup_detail}, {body_contact}, and {visibility}",
|
"{aftercare_act} in {position}, with {cleanup_detail}, {body_contact}, and {visibility}",
|
||||||
"{position} featuring {aftercare_act}, {touch_detail}, {cleanup_detail}, and {reaction_detail}",
|
"{position} featuring {aftercare_act}, {touch_detail}, {cleanup_detail}, and {reaction_detail}",
|
||||||
"{aftercare_act} on {surface}, with {body_contact}, {touch_detail}, and {visibility}",
|
"{aftercare_act} on {surface}, with {body_contact}, {touch_detail}, and {visibility}",
|
||||||
"post-sex aftermath setup: {position}, {aftercare_act}, {cleanup_detail}, and {reaction_detail}",
|
"post-sex aftermath setup with {position}, {aftercare_act}, {cleanup_detail}, and {reaction_detail}",
|
||||||
"{position} while {aftercare_act}, with {touch_detail}, {body_contact}, and {visibility}"
|
"{position} while {aftercare_act}, with {touch_detail}, {body_contact}, and {visibility}"
|
||||||
],
|
],
|
||||||
"item_axes": {
|
"item_axes": {
|
||||||
@@ -950,6 +982,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "penetration",
|
||||||
|
"position_family": "penetrative"
|
||||||
|
},
|
||||||
"scene_pools": ["hardcore_penetrative_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
"scene_pools": ["hardcore_penetrative_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||||
"expression_pools": ["hardcore_penetration_expressions"],
|
"expression_pools": ["hardcore_penetration_expressions"],
|
||||||
"composition_pools": ["penetration_compositions"],
|
"composition_pools": ["penetration_compositions"],
|
||||||
@@ -1123,6 +1159,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "oral",
|
||||||
|
"position_family": "oral"
|
||||||
|
},
|
||||||
"scene_pools": ["hardcore_oral_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
"scene_pools": ["hardcore_oral_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||||
"expression_pools": ["hardcore_oral_expressions"],
|
"expression_pools": ["hardcore_oral_expressions"],
|
||||||
"composition_pools": ["oral_compositions"],
|
"composition_pools": ["oral_compositions"],
|
||||||
@@ -1138,7 +1178,7 @@
|
|||||||
"{oral_act} on {surface}, {hand_detail}, {mouth_detail}, and {climax_hint}",
|
"{oral_act} on {surface}, {hand_detail}, {mouth_detail}, and {climax_hint}",
|
||||||
"{angle} view of {oral_act}, with {visibility}, {body_contact}, and {expression_detail}",
|
"{angle} view of {oral_act}, with {visibility}, {body_contact}, and {expression_detail}",
|
||||||
"{position} while {oral_act}, with {saliva_detail}, {hand_detail}, and {climax_hint}",
|
"{position} while {oral_act}, with {saliva_detail}, {hand_detail}, and {climax_hint}",
|
||||||
"explicit mouth-to-genitals pose: {oral_act}, {mouth_detail}, {body_contact}, and {visibility}"
|
"explicit mouth-to-genitals pose with {oral_act}, {mouth_detail}, {body_contact}, and {visibility}"
|
||||||
],
|
],
|
||||||
"item_axes": {
|
"item_axes": {
|
||||||
"angle": [
|
"angle": [
|
||||||
@@ -1266,6 +1306,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "outercourse",
|
||||||
|
"position_family": "outercourse"
|
||||||
|
},
|
||||||
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||||
"expression_pools": ["hardcore_outercourse_expressions"],
|
"expression_pools": ["hardcore_outercourse_expressions"],
|
||||||
"compositions": [
|
"compositions": [
|
||||||
@@ -1286,7 +1330,7 @@
|
|||||||
},
|
},
|
||||||
"{position} featuring {outer_act}, {body_contact}, {texture_detail}, seen from a {angle} view",
|
"{position} featuring {outer_act}, {body_contact}, {texture_detail}, seen from a {angle} view",
|
||||||
"{angle} view of {outer_act}, with {visibility}, {contact_detail}, and {expression_detail}",
|
"{angle} view of {outer_act}, with {visibility}, {contact_detail}, and {expression_detail}",
|
||||||
"explicit non-penetrative sex pose: {outer_act}, {position}, {contact_detail}, and {visibility}",
|
"explicit non-penetrative sex pose with {outer_act}, {position}, {contact_detail}, and {visibility}",
|
||||||
"{outer_act} on {surface}, with {hand_detail}, {body_contact}, and {texture_detail}",
|
"{outer_act} on {surface}, with {hand_detail}, {body_contact}, and {texture_detail}",
|
||||||
"{position} while {outer_act}, with {texture_detail}, {hand_detail}, and {visibility}"
|
"{position} while {outer_act}, with {texture_detail}, {hand_detail}, and {visibility}"
|
||||||
],
|
],
|
||||||
@@ -1420,6 +1464,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "default",
|
||||||
|
"position_family": "anal"
|
||||||
|
},
|
||||||
"scene_pools": ["hardcore_anal_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
"scene_pools": ["hardcore_anal_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||||
"expression_pools": ["hardcore_anal_dp_expressions"],
|
"expression_pools": ["hardcore_anal_dp_expressions"],
|
||||||
"composition_pools": ["anal_dp_compositions"],
|
"composition_pools": ["anal_dp_compositions"],
|
||||||
@@ -1431,7 +1479,7 @@
|
|||||||
"{double_act} on {surface}, with {leg_detail}, {intensity}, and {climax_hint}",
|
"{double_act} on {surface}, with {leg_detail}, {intensity}, and {climax_hint}",
|
||||||
"{angle} view of {double_act}, {body_arrangement}, {mouth_detail}, and {visibility}",
|
"{angle} view of {double_act}, {body_arrangement}, {mouth_detail}, and {visibility}",
|
||||||
"{anal_act} with {thrust_detail}, {hand_detail}, {body_contact}, and {climax_hint}",
|
"{anal_act} with {thrust_detail}, {hand_detail}, {body_contact}, and {climax_hint}",
|
||||||
"explicit double-contact sex pose: {double_act}, {leg_detail}, {visibility}, and {intensity}"
|
"explicit double-contact sex pose with {double_act}, {leg_detail}, {visibility}, and {intensity}"
|
||||||
],
|
],
|
||||||
"item_axes": {
|
"item_axes": {
|
||||||
"anal_act": [
|
"anal_act": [
|
||||||
@@ -1639,6 +1687,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "default",
|
||||||
|
"position_family": "threesome"
|
||||||
|
},
|
||||||
"scene_pools": ["hardcore_threesome_scenes", "hardcore_group_scenes", "hardcore_mirror_scenes"],
|
"scene_pools": ["hardcore_threesome_scenes", "hardcore_group_scenes", "hardcore_mirror_scenes"],
|
||||||
"expression_pools": ["hardcore_group_expressions"],
|
"expression_pools": ["hardcore_group_expressions"],
|
||||||
"composition_pools": ["threesome_compositions"],
|
"composition_pools": ["threesome_compositions"],
|
||||||
@@ -1646,7 +1698,7 @@
|
|||||||
"{threesome_act} with {body_arrangement}, {oral_detail}, {penetration_detail}, and {visibility}",
|
"{threesome_act} with {body_arrangement}, {oral_detail}, {penetration_detail}, and {visibility}",
|
||||||
"{body_arrangement} while {threesome_act}, with {hand_detail}, {mouth_detail}, and {climax_hint}",
|
"{body_arrangement} while {threesome_act}, with {hand_detail}, {mouth_detail}, and {climax_hint}",
|
||||||
"{angle} threesome view featuring {threesome_act}, {body_contact}, {penetration_detail}, and {visibility}",
|
"{angle} threesome view featuring {threesome_act}, {body_contact}, {penetration_detail}, and {visibility}",
|
||||||
"hardcore threesome pose: {threesome_act}, {body_arrangement}, {oral_detail}, and {climax_hint}",
|
"hardcore threesome pose with {threesome_act}, {body_arrangement}, {oral_detail}, and {climax_hint}",
|
||||||
"{threesome_act} on {surface}, with {hand_detail}, {body_contact}, and {visibility}",
|
"{threesome_act} on {surface}, with {hand_detail}, {body_contact}, and {visibility}",
|
||||||
"{angle} view of {body_arrangement}, {penetration_detail}, {mouth_detail}, and {intensity}",
|
"{angle} view of {body_arrangement}, {penetration_detail}, {mouth_detail}, and {intensity}",
|
||||||
"three-body explicit sex pose with {threesome_act}, {oral_detail}, {hand_detail}, and {visibility}",
|
"three-body explicit sex pose with {threesome_act}, {oral_detail}, {hand_detail}, and {visibility}",
|
||||||
@@ -1822,6 +1874,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "default",
|
||||||
|
"position_family": "group"
|
||||||
|
},
|
||||||
"scene_pools": ["hardcore_group_scenes"],
|
"scene_pools": ["hardcore_group_scenes"],
|
||||||
"expression_pools": ["hardcore_group_expressions"],
|
"expression_pools": ["hardcore_group_expressions"],
|
||||||
"composition_pools": ["group_sex_compositions"],
|
"composition_pools": ["group_sex_compositions"],
|
||||||
@@ -1829,7 +1885,7 @@
|
|||||||
"{group_act} with {arrangement}, {contact_detail}, {fluid_detail}, and {visibility}",
|
"{group_act} with {arrangement}, {contact_detail}, {fluid_detail}, and {visibility}",
|
||||||
"{arrangement} featuring {group_act}, {oral_detail}, {penetration_detail}, and {intensity}",
|
"{arrangement} featuring {group_act}, {oral_detail}, {penetration_detail}, and {intensity}",
|
||||||
"{angle} group-sex view with {group_act}, {contact_detail}, {climax_detail}, and {visibility}",
|
"{angle} group-sex view with {group_act}, {contact_detail}, {climax_detail}, and {visibility}",
|
||||||
"hardcore orgy pose: {arrangement}, {group_act}, {oral_detail}, and {fluid_detail}",
|
"hardcore orgy pose with {arrangement}, {group_act}, {oral_detail}, and {fluid_detail}",
|
||||||
"{group_act} on {surface}, with {penetration_detail}, {contact_detail}, and {visibility}",
|
"{group_act} on {surface}, with {penetration_detail}, {contact_detail}, and {visibility}",
|
||||||
"{angle} view of {arrangement}, {fluid_detail}, {intensity}, and {climax_detail}",
|
"{angle} view of {arrangement}, {fluid_detail}, {intensity}, and {climax_detail}",
|
||||||
"explicit adult group pile with {group_act}, {oral_detail}, {penetration_detail}, and {visibility}",
|
"explicit adult group pile with {group_act}, {oral_detail}, {penetration_detail}, and {visibility}",
|
||||||
@@ -1994,6 +2050,10 @@
|
|||||||
"inherit_expressions": false,
|
"inherit_expressions": false,
|
||||||
"inherit_compositions": false,
|
"inherit_compositions": false,
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
|
"item_template_metadata": {
|
||||||
|
"action_family": "climax",
|
||||||
|
"position_family": "climax"
|
||||||
|
},
|
||||||
"scene_pools": ["hardcore_climax_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
"scene_pools": ["hardcore_climax_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||||
"expression_pools": ["hardcore_climax_expressions"],
|
"expression_pools": ["hardcore_climax_expressions"],
|
||||||
"composition_pools": ["climax_compositions"],
|
"composition_pools": ["climax_compositions"],
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ RANDOM_SUBCATEGORY = "random"
|
|||||||
CATEGORY_PRESETS = {
|
CATEGORY_PRESETS = {
|
||||||
"auto_weighted": ("auto_weighted", RANDOM_SUBCATEGORY),
|
"auto_weighted": ("auto_weighted", RANDOM_SUBCATEGORY),
|
||||||
"auto_full": ("auto_full", RANDOM_SUBCATEGORY),
|
"auto_full": ("auto_full", RANDOM_SUBCATEGORY),
|
||||||
|
"woman": ("woman", RANDOM_SUBCATEGORY),
|
||||||
|
"man": ("man", RANDOM_SUBCATEGORY),
|
||||||
|
"couple": ("couple", RANDOM_SUBCATEGORY),
|
||||||
|
"group_or_layout": ("group_or_layout", RANDOM_SUBCATEGORY),
|
||||||
"women_casual": ("Casual clothes", RANDOM_SUBCATEGORY),
|
"women_casual": ("Casual clothes", RANDOM_SUBCATEGORY),
|
||||||
"men_casual": ("Men casual clothes", RANDOM_SUBCATEGORY),
|
"men_casual": ("Men casual clothes", RANDOM_SUBCATEGORY),
|
||||||
"couple_casual": ("Couple casual clothes", RANDOM_SUBCATEGORY),
|
"couple_casual": ("Couple casual clothes", RANDOM_SUBCATEGORY),
|
||||||
|
|||||||
@@ -113,5 +113,5 @@ def subcategory_choices() -> list[str]:
|
|||||||
choices = [category_policy.RANDOM_SUBCATEGORY]
|
choices = [category_policy.RANDOM_SUBCATEGORY]
|
||||||
for category in category_policy.load_category_library():
|
for category in category_policy.load_category_library():
|
||||||
for subcategory in category["subcategories"]:
|
for subcategory in category["subcategories"]:
|
||||||
choices.append(f"{category['name']} / {subcategory['name']}")
|
choices.append(category_policy.exact_subcategory_selector(category, subcategory))
|
||||||
return choices
|
return choices
|
||||||
|
|||||||
+29
-4
@@ -422,6 +422,30 @@ def find_category(categories: list[dict[str, Any]], name_or_slug: str) -> dict[s
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def exact_subcategory_selector(category: dict[str, Any], subcategory: dict[str, Any]) -> str:
|
||||||
|
return f"{category.get('name')} / {subcategory.get('name')}"
|
||||||
|
|
||||||
|
|
||||||
|
def split_exact_subcategory_choice(
|
||||||
|
categories: list[dict[str, Any]],
|
||||||
|
subcategory_choice: str,
|
||||||
|
) -> tuple[dict[str, Any], str] | None:
|
||||||
|
choice = str(subcategory_choice or "").strip()
|
||||||
|
if not choice or " / " not in choice:
|
||||||
|
return None
|
||||||
|
candidates: list[tuple[int, dict[str, Any], str]] = []
|
||||||
|
for category in categories:
|
||||||
|
for category_label in (category.get("name", ""), category.get("slug", "")):
|
||||||
|
category_label = str(category_label).strip()
|
||||||
|
prefix = f"{category_label} / "
|
||||||
|
if category_label and choice.lower().startswith(prefix.lower()):
|
||||||
|
candidates.append((len(prefix), category, choice[len(prefix) :].strip()))
|
||||||
|
if candidates:
|
||||||
|
_length, category, subcategory_name = max(candidates, key=lambda candidate: candidate[0])
|
||||||
|
return category, subcategory_name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _base_cast_counts(women_count: int, men_count: int) -> tuple[int, int]:
|
def _base_cast_counts(women_count: int, men_count: int) -> tuple[int, int]:
|
||||||
women_count = max(0, int(women_count))
|
women_count = max(0, int(women_count))
|
||||||
men_count = max(0, int(men_count))
|
men_count = max(0, int(men_count))
|
||||||
@@ -467,10 +491,11 @@ def find_subcategory(
|
|||||||
) -> tuple[dict[str, Any], dict[str, Any], int, int]:
|
) -> tuple[dict[str, Any], dict[str, Any], int, int]:
|
||||||
women_count, men_count = _base_cast_counts(women_count, men_count)
|
women_count, men_count = _base_cast_counts(women_count, men_count)
|
||||||
if subcategory_choice and subcategory_choice != random_subcategory and " / " in subcategory_choice:
|
if subcategory_choice and subcategory_choice != random_subcategory and " / " in subcategory_choice:
|
||||||
category_name, subcategory_name = subcategory_choice.split(" / ", 1)
|
exact_choice = split_exact_subcategory_choice(categories, subcategory_choice)
|
||||||
category = find_category(categories, category_name)
|
if not exact_choice:
|
||||||
if not category:
|
category_name = str(subcategory_choice).split(" / ", 1)[0]
|
||||||
raise ValueError(f"Unknown category in subcategory picker: {category_name}")
|
raise ValueError(f"Unknown category in subcategory picker: {category_name}")
|
||||||
|
category, subcategory_name = exact_choice
|
||||||
wanted = subcategory_name.strip().lower()
|
wanted = subcategory_name.strip().lower()
|
||||||
for subcategory in category["subcategories"]:
|
for subcategory in category["subcategories"]:
|
||||||
if subcategory["name"].lower() == wanted or subcategory["slug"].lower() == wanted:
|
if subcategory["name"].lower() == wanted or subcategory["slug"].lower() == wanted:
|
||||||
@@ -485,7 +510,7 @@ def find_subcategory(
|
|||||||
f"women_count={women_count}, men_count={men_count}"
|
f"women_count={women_count}, men_count={men_count}"
|
||||||
)
|
)
|
||||||
return category, subcategory, adjusted_women_count, adjusted_men_count
|
return category, subcategory, adjusted_women_count, adjusted_men_count
|
||||||
raise ValueError(f"Unknown subcategory '{subcategory_name}' for category '{category_name}'")
|
raise ValueError(f"Unknown subcategory '{subcategory_name}' for category '{category['name']}'")
|
||||||
|
|
||||||
if category_choice == "custom_random":
|
if category_choice == "custom_random":
|
||||||
if not categories:
|
if not categories:
|
||||||
|
|||||||
@@ -34,6 +34,43 @@ def template_metadata(item: Any) -> dict[str, Any]:
|
|||||||
return {key: item[key] for key in TEMPLATE_METADATA_KEYS if key in item}
|
return {key: item[key] for key in TEMPLATE_METADATA_KEYS if key in item}
|
||||||
|
|
||||||
|
|
||||||
|
def merge_template_metadata(*metadata_values: Any) -> dict[str, Any]:
|
||||||
|
merged: dict[str, Any] = {}
|
||||||
|
for value in metadata_values:
|
||||||
|
metadata = template_metadata(value)
|
||||||
|
if not metadata:
|
||||||
|
continue
|
||||||
|
for key in ("action_family", "action_type", "family", "position_family", "position_key"):
|
||||||
|
if str(metadata.get(key) or "").strip():
|
||||||
|
merged[key] = metadata[key]
|
||||||
|
if metadata.get("position_keys") is not None:
|
||||||
|
merged["position_keys"] = merge_position_keys(
|
||||||
|
template_position_keys(merged),
|
||||||
|
template_position_keys(metadata),
|
||||||
|
)
|
||||||
|
hint_map = formatter_hints(metadata)
|
||||||
|
if hint_map:
|
||||||
|
existing = formatter_hints(merged)
|
||||||
|
for route, hints in hint_map.items():
|
||||||
|
for hint in hints:
|
||||||
|
if hint not in existing.setdefault(route, []):
|
||||||
|
existing[route].append(hint)
|
||||||
|
merged["formatter_hint"] = existing
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def inherited_template_metadata(*containers: Any) -> dict[str, Any]:
|
||||||
|
metadata_parts: list[dict[str, Any]] = []
|
||||||
|
for container in containers:
|
||||||
|
if not isinstance(container, dict):
|
||||||
|
continue
|
||||||
|
nested = container.get("item_template_metadata")
|
||||||
|
if isinstance(nested, dict):
|
||||||
|
metadata_parts.append(nested)
|
||||||
|
metadata_parts.append(container)
|
||||||
|
return merge_template_metadata(*metadata_parts)
|
||||||
|
|
||||||
|
|
||||||
def template_position_family(metadata: dict[str, Any]) -> str:
|
def template_position_family(metadata: dict[str, Any]) -> str:
|
||||||
return normalize_hardcore_position_family(
|
return normalize_hardcore_position_family(
|
||||||
metadata.get("position_family") or metadata.get("family"),
|
metadata.get("position_family") or metadata.get("family"),
|
||||||
@@ -105,8 +142,10 @@ def formatter_hints_for_route(row_or_hints: Any, route: str) -> list[str]:
|
|||||||
raw_hints = row_or_hints.get("formatter_hints") or {}
|
raw_hints = row_or_hints.get("formatter_hints") or {}
|
||||||
elif "formatter_hint" in row_or_hints:
|
elif "formatter_hint" in row_or_hints:
|
||||||
raw_hints = formatter_hints(row_or_hints)
|
raw_hints = formatter_hints(row_or_hints)
|
||||||
else:
|
elif row_or_hints and all(normalize_formatter_route(raw_route) for raw_route in row_or_hints):
|
||||||
raw_hints = row_or_hints
|
raw_hints = row_or_hints
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
normalized: dict[str, list[str]] = {}
|
normalized: dict[str, list[str]] = {}
|
||||||
if isinstance(raw_hints, dict):
|
if isinstance(raw_hints, dict):
|
||||||
|
|||||||
@@ -21,6 +21,31 @@ The map audit currently sees:
|
|||||||
- 23 expression pools.
|
- 23 expression pools.
|
||||||
- 24 composition pools.
|
- 24 composition pools.
|
||||||
- A new Krea2 resolution node with width/height/API aspect outputs.
|
- A new Krea2 resolution node with width/height/API aspect outputs.
|
||||||
|
- Registered route policy validation, so action/position families stay covered
|
||||||
|
by SDXL family tags, caption labels, and SDXL incompatibility-filter keys.
|
||||||
|
- Route simulation family coverage, so representative generated rows exercise
|
||||||
|
every registered action and position family except documented special cases
|
||||||
|
covered by dedicated smoke fixtures.
|
||||||
|
- Pair seed simulation, so Insta/OF soft/hard metadata and formatter outputs
|
||||||
|
prove locked determinism and person/scene/content/pose/expression/
|
||||||
|
composition reroll behavior.
|
||||||
|
- Formatter route traces expose selected metadata fields, so Krea2, SDXL, and
|
||||||
|
caption outputs can be debugged by category, action/position family, selected
|
||||||
|
pair side, scene profile, position keys, and POV labels instead of only
|
||||||
|
proving that a metadata branch was used.
|
||||||
|
- Insta/OF side-target training captions no longer prepend shared cast
|
||||||
|
descriptors when the selected side row already emits its own cast prose, and
|
||||||
|
route simulation flags repeated cast descriptors.
|
||||||
|
- Route simulation now has an opt-in multi-seed sweep, and the smoke suite runs
|
||||||
|
a three-seed sweep so representative route/noise checks are not proven by one
|
||||||
|
lucky seed only.
|
||||||
|
- Route simulation now emits a `quality` summary that groups route health by
|
||||||
|
target, action family, and position family, separates route issues from
|
||||||
|
coverage/seed-check issues, buckets issue types, and reports weakest cases so
|
||||||
|
future prompt-logic passes can target the worst path first.
|
||||||
|
- Map audit now fails when a registered ComfyUI node display name is missing
|
||||||
|
from the route map or README, so utility nodes cannot silently drift out of
|
||||||
|
user-facing documentation.
|
||||||
|
|
||||||
## Architectural Finding
|
## Architectural Finding
|
||||||
|
|
||||||
@@ -53,6 +78,7 @@ It should only handle route-agnostic cleanup:
|
|||||||
- empty field-label removal;
|
- empty field-label removal;
|
||||||
- repeated trigger prefix cleanup;
|
- repeated trigger prefix cleanup;
|
||||||
- duplicate comma-list item removal;
|
- duplicate comma-list item removal;
|
||||||
|
- route-agnostic negative-prompt merge/dedupe;
|
||||||
- adjacent duplicate sentence cleanup;
|
- adjacent duplicate sentence cleanup;
|
||||||
- simple dangling connector cleanup.
|
- simple dangling connector cleanup.
|
||||||
|
|
||||||
@@ -69,6 +95,8 @@ Formatter input/fallback parsing now has one home:
|
|||||||
It owns route-neutral parsing shared by Krea2, SDXL, and natural-caption
|
It owns route-neutral parsing shared by Krea2, SDXL, and natural-caption
|
||||||
routes:
|
routes:
|
||||||
|
|
||||||
|
- input-hint choice lists and normalization for `auto`, `metadata_json`, and
|
||||||
|
route-specific text modes;
|
||||||
- whitespace and punctuation normalization before formatter parsing;
|
- whitespace and punctuation normalization before formatter parsing;
|
||||||
- JSON row detection from `metadata_json` or source text;
|
- JSON row detection from `metadata_json` or source text;
|
||||||
- trigger-prefix stripping with route-specific trigger candidate lists;
|
- trigger-prefix stripping with route-specific trigger candidate lists;
|
||||||
@@ -82,6 +110,25 @@ routes:
|
|||||||
It must not make formatter-style decisions. Krea prose, SDXL tags, and training
|
It must not make formatter-style decisions. Krea prose, SDXL tags, and training
|
||||||
caption sentence shape stay in their formatter modules.
|
caption sentence shape stay in their formatter modules.
|
||||||
|
|
||||||
|
Formatter detail-level handling now has one home:
|
||||||
|
|
||||||
|
- `formatter_detail.py`
|
||||||
|
|
||||||
|
It owns route-neutral prose detail levels, node choice lists, normalization, and
|
||||||
|
the concise/balanced/dense inclusion gate used by Krea2 and natural-caption
|
||||||
|
routes. It must not own route-specific style controls such as Krea photographic
|
||||||
|
mode or caption style-tail policy.
|
||||||
|
|
||||||
|
Formatter target handling now has one home:
|
||||||
|
|
||||||
|
- `formatter_target.py`
|
||||||
|
|
||||||
|
It owns route-neutral target normalization for `auto`, `single`, `softcore`,
|
||||||
|
and `hardcore`, including node choice lists and pair-side semantics.
|
||||||
|
Single-output formatters select the softcore side for pair `auto`/`single`
|
||||||
|
targets, while caption pair routing can still include both sides for combined
|
||||||
|
training captions.
|
||||||
|
|
||||||
Shared hardcore phrase cleanup now has one home:
|
Shared hardcore phrase cleanup now has one home:
|
||||||
|
|
||||||
- `hardcore_text_cleanup.py`
|
- `hardcore_text_cleanup.py`
|
||||||
@@ -120,6 +167,13 @@ Move or isolate later:
|
|||||||
|
|
||||||
Already isolated:
|
Already isolated:
|
||||||
|
|
||||||
|
- single-prompt builder orchestration, including input normalization, seed-axis
|
||||||
|
setup, built-in/custom row routing, legacy location/composition handling,
|
||||||
|
camera application, and final prompt-row normalization, lives in
|
||||||
|
`builder_prompt_route.py`; `prompt_builder.py` keeps the public wrapper.
|
||||||
|
- config-driven prompt-builder request parsing, helper-node config mapping, and
|
||||||
|
direct `build_prompt` kwarg assembly live in `builder_config_route.py`;
|
||||||
|
`prompt_builder.py` keeps the public wrapper.
|
||||||
- JSON category loading, subcategory normalization, named scene/expression/
|
- JSON category loading, subcategory normalization, named scene/expression/
|
||||||
composition pool loading, cast compatibility filtering, exact subcategory
|
composition pool loading, cast compatibility filtering, exact subcategory
|
||||||
lookup, and inheritance-based pool merging live in `category_library.py`.
|
lookup, and inheritance-based pool merging live in `category_library.py`.
|
||||||
@@ -129,8 +183,11 @@ Already isolated:
|
|||||||
normalization, position-key normalization, and metadata audit errors live in
|
normalization, position-key normalization, and metadata audit errors live in
|
||||||
`category_template_metadata.py`.
|
`category_template_metadata.py`.
|
||||||
- row item selection, weighted item/pair choice, item-template axis filling,
|
- row item selection, weighted item/pair choice, item-template axis filling,
|
||||||
and oral/outercourse axis compatibility filters live in `row_item.py`;
|
and oral/outercourse/anal axis compatibility filters live in `row_item.py`;
|
||||||
`prompt_builder.py` keeps public delegate wrappers.
|
`prompt_builder.py` keeps public delegate wrappers.
|
||||||
|
- outercourse action-kind classification for boobjob, testicle-sucking,
|
||||||
|
penis-licking, handjob, and footjob lives in `outercourse_action_policy.py`
|
||||||
|
and is shared by row item filtering, role graphs, and Krea action cleanup.
|
||||||
- row category/subcategory/item route resolution lives in
|
- row category/subcategory/item route resolution lives in
|
||||||
`row_category_route.py` behind `CategoryItemRoute`, covering hardcore
|
`row_category_route.py` behind `CategoryItemRoute`, covering hardcore
|
||||||
position-category filtering, cast-count adjustment, pose-vs-content seed-axis
|
position-category filtering, cast-count adjustment, pose-vs-content seed-axis
|
||||||
@@ -228,7 +285,8 @@ Already isolated:
|
|||||||
dominant guidance, camera performance, aftercare, and group coordination.
|
dominant guidance, camera performance, aftercare, and group coordination.
|
||||||
- outercourse-specific role graph wording has started moving into action-family
|
- outercourse-specific role graph wording has started moving into action-family
|
||||||
modules; `hardcore_role_outercourse.py` owns boobjob, testicle-sucking,
|
modules; `hardcore_role_outercourse.py` owns boobjob, testicle-sucking,
|
||||||
penis-licking, handjob, and footjob body geometry.
|
penis-licking, handjob, and footjob body geometry, keyed by
|
||||||
|
`outercourse_action_policy.py`.
|
||||||
- oral-specific role graph wording lives in `hardcore_role_oral.py`, including
|
- oral-specific role graph wording lives in `hardcore_role_oral.py`, including
|
||||||
direct POV viewer phrasing for kneeling, face-sitting, sixty-nine,
|
direct POV viewer phrasing for kneeling, face-sitting, sixty-nine,
|
||||||
edge-supported, side-lying, chair, standing, and reclining oral positions.
|
edge-supported, side-lying, chair, standing, and reclining oral positions.
|
||||||
@@ -243,9 +301,10 @@ Already isolated:
|
|||||||
side-lying, and front/back group layouts.
|
side-lying, and front/back group layouts.
|
||||||
- camera option schema, orbit/Qwen translation, config parsing, camera
|
- camera option schema, orbit/Qwen translation, config parsing, camera
|
||||||
directive text, and camera caption text live in `camera_config.py`;
|
directive text, and camera caption text live in `camera_config.py`;
|
||||||
camera-scene prose lives in `scene_camera_adapters.py`; row-level camera
|
camera-scene prose and contextual scene composition mutation for coworking,
|
||||||
insertion, contextual coworking composition mutation, subject-kind detection,
|
library, and semi-public profiles live in `scene_camera_adapters.py`;
|
||||||
and POV suppression live in `row_camera.py`.
|
row-level camera insertion, subject-kind detection, and POV suppression live
|
||||||
|
in `row_camera.py`.
|
||||||
- shared POV slot detection, label merging/filtering, builder-side POV
|
- shared POV slot detection, label merging/filtering, builder-side POV
|
||||||
directives, source role-graph viewer replacement, and shared composition
|
directives, source role-graph viewer replacement, and shared composition
|
||||||
cleanup live in `pov_policy.py`; prompt builder and Krea POV routes delegate
|
cleanup live in `pov_policy.py`; prompt builder and Krea POV routes delegate
|
||||||
@@ -266,7 +325,7 @@ Already isolated:
|
|||||||
caption-part joining, embedded soft/hard row output synchronization, and row
|
caption-part joining, embedded soft/hard row output synchronization, and row
|
||||||
sanitation before metadata leaves generation. It also copies side-specific
|
sanitation before metadata leaves generation. It also copies side-specific
|
||||||
pair metadata, such as soft partner styling and hardcore clothing/detail
|
pair metadata, such as soft partner styling and hardcore clothing/detail
|
||||||
state, onto the embedded soft/hard rows.
|
state, plus shared cast descriptors, onto the embedded soft/hard rows.
|
||||||
- final custom-row assembly now lives in `row_assembly.py` behind
|
- final custom-row assembly now lives in `row_assembly.py` behind
|
||||||
`CustomRowAssemblyRequest`, covering render context population,
|
`CustomRowAssemblyRequest`, covering render context population,
|
||||||
prompt/caption rendering delegation, row-base indexing, row metadata copying,
|
prompt/caption rendering delegation, row-base indexing, row metadata copying,
|
||||||
@@ -302,6 +361,9 @@ Already isolated:
|
|||||||
prose, descriptor-entry assembly, shared descriptors, cast-label cleanup,
|
prose, descriptor-entry assembly, shared descriptors, cast-label cleanup,
|
||||||
same-cast softcore descriptor text, partner styling, platform and level
|
same-cast softcore descriptor text, partner styling, platform and level
|
||||||
labels, softcore cast presence text, and hard cast summary text.
|
labels, softcore cast presence text, and hard cast summary text.
|
||||||
|
- shared softcore pair prose for solo/same-cast/POV presence, caption side
|
||||||
|
wording, and creator-shot teaser directives lives in
|
||||||
|
`softcore_text_policy.py`; pair, Krea, and caption routes delegate to it.
|
||||||
- pair-level camera routing lives in `pair_camera.py` behind
|
- pair-level camera routing lives in `pair_camera.py` behind
|
||||||
`InstaPairCameraRoute`, including soft/hard camera config selection,
|
`InstaPairCameraRoute`, including soft/hard camera config selection,
|
||||||
same-as-softcore mode, camera-detail override, same-room hard scene
|
same-as-softcore mode, camera-detail override, same-room hard scene
|
||||||
@@ -319,8 +381,9 @@ Already isolated:
|
|||||||
Embedded soft/hard rows are synchronized to the final pair prompt, caption,
|
Embedded soft/hard rows are synchronized to the final pair prompt, caption,
|
||||||
and negative outputs during normalization so serialized pair metadata does
|
and negative outputs during normalization so serialized pair metadata does
|
||||||
not carry stale standalone row text. Side-specific structured fields are
|
not carry stale standalone row text. Side-specific structured fields are
|
||||||
synchronized there too, including soft partner styling and hardcore clothing
|
synchronized there too, including soft partner styling, hardcore clothing
|
||||||
continuity metadata.
|
continuity metadata, and shared cast descriptors for same-cast caption and
|
||||||
|
formatter routes.
|
||||||
|
|
||||||
### Krea2 Formatter Path
|
### Krea2 Formatter Path
|
||||||
|
|
||||||
@@ -335,6 +398,11 @@ Keep here:
|
|||||||
|
|
||||||
Already isolated:
|
Already isolated:
|
||||||
|
|
||||||
|
- `krea_format_route.py` owns top-level Krea dispatch, including option
|
||||||
|
normalization, metadata-vs-text input selection, single-vs-pair branching,
|
||||||
|
shared target normalization via `formatter_target.py`, extra
|
||||||
|
positive/negative merging, final prose hygiene, and output shape;
|
||||||
|
`krea_formatter.py` keeps the public wrapper.
|
||||||
- `krea_configured_cast_formatter.py` owns normal metadata configured-cast
|
- `krea_configured_cast_formatter.py` owns normal metadata configured-cast
|
||||||
Krea prose assembly behind `KreaConfiguredCastRequest`,
|
Krea prose assembly behind `KreaConfiguredCastRequest`,
|
||||||
`KreaConfiguredCastDependencies`, and `KreaConfiguredCastPrompt`;
|
`KreaConfiguredCastDependencies`, and `KreaConfiguredCastPrompt`;
|
||||||
@@ -404,6 +472,11 @@ Keep here:
|
|||||||
|
|
||||||
Already isolated:
|
Already isolated:
|
||||||
|
|
||||||
|
- `sdxl_format_route.py` owns top-level SDXL dispatch, including formatter
|
||||||
|
profile application, shared target normalization via `formatter_target.py`,
|
||||||
|
nude-weight normalization, metadata-vs-text input selection, single-vs-pair
|
||||||
|
branching, final prompt/negative output shape, and fallback routing;
|
||||||
|
`sdxl_formatter.py` keeps the public wrapper.
|
||||||
- `sdxl_tag_routes.py` owns normal metadata row tags and Insta/OF pair soft/hard
|
- `sdxl_tag_routes.py` owns normal metadata row tags and Insta/OF pair soft/hard
|
||||||
tag extraction behind `SDXLRowTagRequest`, `SDXLPairTagRequest`,
|
tag extraction behind `SDXLRowTagRequest`, `SDXLPairTagRequest`,
|
||||||
`SDXLTagRouteDependencies`, and `SDXLTagRoute`; `sdxl_formatter.py` keeps
|
`SDXLTagRouteDependencies`, and `SDXLTagRoute`; `sdxl_formatter.py` keeps
|
||||||
@@ -438,6 +511,11 @@ Keep here:
|
|||||||
|
|
||||||
Already isolated:
|
Already isolated:
|
||||||
|
|
||||||
|
- `caption_format_route.py` owns top-level caption dispatch, including input
|
||||||
|
hint normalization, shared target normalization via `formatter_target.py`,
|
||||||
|
caption profile application, metadata-vs-text branching, trigger wrapping,
|
||||||
|
final prose hygiene, and method/output shape;
|
||||||
|
`caption_naturalizer.py` keeps the public wrapper.
|
||||||
- `caption_metadata_routes.py` owns metadata row natural-language assembly for
|
- `caption_metadata_routes.py` owns metadata row natural-language assembly for
|
||||||
single, couple, configured-cast, group/layout, and Insta/OF pair routes behind
|
single, couple, configured-cast, group/layout, and Insta/OF pair routes behind
|
||||||
`CaptionMetadataRouteRequest`, `CaptionMetadataRouteDependencies`, and
|
`CaptionMetadataRouteRequest`, `CaptionMetadataRouteDependencies`, and
|
||||||
@@ -482,8 +560,10 @@ Keep here:
|
|||||||
Improve later:
|
Improve later:
|
||||||
|
|
||||||
- keep `tools/prompt_map_audit.py` passing; it now checks referenced
|
- keep `tools/prompt_map_audit.py` passing; it now checks referenced
|
||||||
expression/composition/scene pools, item-template axes, and object-template
|
expression/composition/scene pools, item-template axes, object-template
|
||||||
metadata values for both string and object templates.
|
metadata values for both string and object templates, category/subcategory
|
||||||
|
identity uniqueness, registered formatter policy coverage for route
|
||||||
|
families, and critical route documentation plus expected smoke coverage.
|
||||||
|
|
||||||
### Node / UI Path
|
### Node / UI Path
|
||||||
|
|
||||||
@@ -545,6 +625,10 @@ Already isolated:
|
|||||||
- node input tooltip inventory, node-specific tooltip overrides, dynamic input
|
- node input tooltip inventory, node-specific tooltip overrides, dynamic input
|
||||||
fallback tooltip rules, and tooltip injection live in `node_tooltips.py`;
|
fallback tooltip rules, and tooltip injection live in `node_tooltips.py`;
|
||||||
`__init__.py` only applies the installer to the assembled node registry.
|
`__init__.py` only applies the installer to the assembled node registry.
|
||||||
|
- node registration drift is checked by `tools/prompt_map_audit.py`: concrete
|
||||||
|
`SxCP...` node classes in node modules must be present in their module class
|
||||||
|
mappings and matching display-name mappings before they can silently
|
||||||
|
disappear from ComfyUI.
|
||||||
- profile-save and accumulator server payload handling lives in
|
- profile-save and accumulator server payload handling lives in
|
||||||
`server_routes.py`; `__init__.py` only wires those pure handlers to ComfyUI
|
`server_routes.py`; `__init__.py` only wires those pure handlers to ComfyUI
|
||||||
JSON responses, and `tools/prompt_smoke.py` covers the handlers without
|
JSON responses, and `tools/prompt_smoke.py` covers the handlers without
|
||||||
@@ -570,8 +654,11 @@ Near-term:
|
|||||||
|
|
||||||
Medium-term:
|
Medium-term:
|
||||||
|
|
||||||
- Extract category loading and role graph logic.
|
- Keep category loading, prompt-row routing, and role-graph routing in their
|
||||||
- Convert keyword-heavy interaction filtering to template metadata.
|
extracted owner modules instead of rebuilding them inside
|
||||||
|
`prompt_builder.py`.
|
||||||
|
- Add new template metadata only when a generated route needs a concrete action,
|
||||||
|
position, or formatter hint that is not already expressible.
|
||||||
|
|
||||||
### Insta/OF Pair
|
### Insta/OF Pair
|
||||||
|
|
||||||
@@ -583,12 +670,16 @@ Near-term:
|
|||||||
scene/camera/clothing fields.
|
scene/camera/clothing fields.
|
||||||
- Keep same-room pair continuity synchronized in both assembled prompt text and
|
- Keep same-room pair continuity synchronized in both assembled prompt text and
|
||||||
`hardcore_row.scene_text`; `tools/prompt_smoke.py` covers this drift case.
|
`hardcore_row.scene_text`; `tools/prompt_smoke.py` covers this drift case.
|
||||||
|
- Keep pair seed behavior synchronized across soft/hard rows; the route
|
||||||
|
simulator now checks locked pair determinism and person, scene, content,
|
||||||
|
pose, expression, and composition rerolls.
|
||||||
|
|
||||||
Medium-term:
|
Medium-term:
|
||||||
|
|
||||||
- Make pair camera and clothing phases explicit subfunctions.
|
- Keep camera and clothing phases in `pair_camera.py` and `pair_clothing.py`;
|
||||||
- Add smoke fixtures for same-cast, POV man, explicit nude, and different-camera
|
extend those modules when a pair output shows concrete camera/clothing drift.
|
||||||
modes.
|
- Add pair smoke fixtures only when a new pair option or observed output changes
|
||||||
|
soft/hard cast, POV, explicit-nude, or split-camera metadata behavior.
|
||||||
|
|
||||||
### Krea2
|
### Krea2
|
||||||
|
|
||||||
@@ -613,11 +704,16 @@ Near-term:
|
|||||||
outputs so source contact/guidance/presentation wording stays metadata-driven.
|
outputs so source contact/guidance/presentation wording stays metadata-driven.
|
||||||
- Cover generated fallback role routes through Krea, SDXL, and natural caption
|
- Cover generated fallback role routes through Krea, SDXL, and natural caption
|
||||||
outputs so solo and same-sex paths do not remain untested edge behavior.
|
outputs so solo and same-sex paths do not remain untested edge behavior.
|
||||||
|
- Keep route simulation coverage updated when adding action or position
|
||||||
|
families, so generated Krea2, SDXL, and natural-caption paths prove the new
|
||||||
|
family reaches formatter metadata routes.
|
||||||
|
|
||||||
Medium-term:
|
Medium-term:
|
||||||
|
|
||||||
- Dispatch action rewriting by action family.
|
- Keep action-family rewriting dispatched through `krea_action_dispatch.py` and
|
||||||
- Continue splitting remaining Krea semantic helpers into smaller modules.
|
the existing `krea_action_*` / `krea_pov*` helpers.
|
||||||
|
- Add or split Krea helpers only for an observed route failure or a new
|
||||||
|
action-family metadata path.
|
||||||
|
|
||||||
### SDXL
|
### SDXL
|
||||||
|
|
||||||
@@ -673,9 +769,10 @@ Medium-term:
|
|||||||
|
|
||||||
## Recommended Next Passes
|
## Recommended Next Passes
|
||||||
|
|
||||||
1. Continue splitting remaining `__init__.py` node classes by family after
|
1. Keep new node classes in their owning `node_*.py` or `loop_nodes.py`
|
||||||
behavior is covered by smoke checks.
|
module, with registration/display docs covered by the audit.
|
||||||
2. Continue splitting the internals of `hardcore_role_graphs.py` by action
|
2. Keep `hardcore_role_graphs.py` as the dispatch surface; add behavior in the
|
||||||
family once generated edge cases are covered by smoke fixtures.
|
existing `hardcore_role_*` action-family modules only when a concrete
|
||||||
3. Add more route-level smoke fixtures for generated edge cases that are not
|
generated edge case needs it.
|
||||||
covered by the current static Krea/SDXL/caption metadata fixtures.
|
3. Add route-level smoke fixtures only for observed generated edge cases or new
|
||||||
|
metadata fields that affect Krea2, SDXL, or caption output.
|
||||||
|
|||||||
+237
-42
@@ -56,25 +56,41 @@ flowchart TD
|
|||||||
The config nodes mostly emit JSON. The final builder nodes parse that JSON and
|
The config nodes mostly emit JSON. The final builder nodes parse that JSON and
|
||||||
call the same core generation functions.
|
call the same core generation functions.
|
||||||
|
|
||||||
|
For Insta/OF pair formatting, the embedded `hardcore_row` is authoritative for
|
||||||
|
hardcore scene/composition text. Same-room continuity may copy the soft scene
|
||||||
|
into that row earlier, but formatter routes should not bypass later hard-row
|
||||||
|
cleanup such as clothing/body-access scene sanitization.
|
||||||
|
|
||||||
## Main Entry Points
|
## Main Entry Points
|
||||||
|
|
||||||
| ComfyUI node | Python entry | What it owns |
|
| ComfyUI node | Python entry | What it owns |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `SxCP Prompt Builder` | `build_prompt` | Direct single prompt generation. Can use built-in categories or JSON categories. |
|
| `SxCP Prompt Builder` | `build_prompt` -> `builder_prompt_route.py` | Direct single prompt generation. Can use built-in categories or JSON categories. |
|
||||||
| `SxCP Prompt Builder From Configs` | `build_prompt_from_configs` -> `build_prompt` | Same generator, but inputs come from category/cast/profile/filter helper nodes. |
|
| `SxCP Prompt Builder From Configs` | `build_prompt_from_configs` -> `builder_config_route.py` -> `build_prompt` -> `builder_prompt_route.py` | Same generator, but inputs come from category/cast/profile/filter helper nodes. |
|
||||||
| `SxCP Insta/OF Prompt Pair` | `build_insta_of_pair` | Builds a softcore row and hardcore row with shared cast/continuity options. |
|
| `SxCP Insta/OF Prompt Pair` | `build_insta_of_pair` | Builds a softcore row and hardcore row with shared cast/continuity options. |
|
||||||
| `SxCP Krea2 Formatter` | `format_krea2_prompt` | Converts metadata rows or pair metadata into Krea2-friendly prose. |
|
| `SxCP Scene Start` / `SxCP Scene Output` / `SxCP Scene Pair Output` | `node_scene.py` -> existing builder and pair routes | v2 structured `SXCP_SCENE` chain. Layers are split into cast, character, wardrobe, location, set dressing, blocking, action, performance, camera, composition, and lighting before compatibility rendering. |
|
||||||
| `SxCP SDXL Formatter` | `format_sdxl_prompt` | Converts metadata rows or pair metadata into SDXL/tag style prompts. |
|
| `SxCP Krea2 Formatter` | `format_krea2_prompt` -> `krea_format_route.py` | Converts metadata rows or pair metadata into Krea2-friendly prose. |
|
||||||
| `SxCP Caption Naturalizer` | `naturalize_caption` | Converts rows into more natural sentence captions. |
|
| `SxCP SDXL Formatter` | `format_sdxl_prompt` -> `sdxl_format_route.py` | Converts metadata rows or pair metadata into SDXL/tag style prompts. |
|
||||||
|
| `SxCP Caption Naturalizer` | `naturalize_caption` -> `caption_format_route.py` | Converts rows into more natural sentence captions. |
|
||||||
|
|
||||||
|
V2 scene-chain display nodes: `SxCP Scene Cast`, `SxCP Scene Character`,
|
||||||
|
`SxCP Scene Wardrobe`, `SxCP Scene Location`, `SxCP Scene Set Dressing`,
|
||||||
|
`SxCP Scene Blocking`, `SxCP Scene Action`, `SxCP Scene Performance`,
|
||||||
|
`SxCP Scene Camera`, `SxCP Scene Composition`, `SxCP Scene Lighting`,
|
||||||
|
`SxCP Scene Branch Pair`, `SxCP Softcore Branch Options`, and
|
||||||
|
`SxCP Hardcore Branch Options`.
|
||||||
|
|
||||||
Core helper ownership:
|
Core helper ownership:
|
||||||
|
|
||||||
| Python module | What it owns |
|
| Python module | What it owns |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `category_library.py` | JSON category loading, subcategory normalization, named scene/expression/composition pool loading, cast compatibility filtering, exact subcategory lookup, and inheritance-based pool merging. |
|
| `category_library.py` | JSON category loading, subcategory normalization, named scene/expression/composition pool loading, cast compatibility filtering, exact subcategory lookup, and inheritance-based pool merging. |
|
||||||
|
| `builder_prompt_route.py` | Single-prompt builder orchestration, input normalization, seed-axis setup, built-in/custom row routing, legacy location/composition handling, camera application, and final prompt-row normalization. |
|
||||||
|
| `builder_config_route.py` | Config-driven prompt-builder request parsing, category/cast/profile/filter helper-node mapping, and direct `build_prompt` kwarg assembly. |
|
||||||
| `category_extensions.py` | JSON `pool_extensions`, legacy pool patching, built-in category choice lists, and category/subcategory UI choices. |
|
| `category_extensions.py` | JSON `pool_extensions`, legacy pool patching, built-in category choice lists, and category/subcategory UI choices. |
|
||||||
| `category_template_metadata.py` | Object-style item-template metadata extraction, action/position family normalization, position-key normalization, key merging, and audit validation errors. |
|
| `category_template_metadata.py` | Object-style and inherited item-template metadata extraction, action/position family normalization, position-key normalization, key merging, formatter-hint merging, and audit validation errors. |
|
||||||
| `row_item.py` | Row item selection, weighted item/pair choice, item-template axis filling, and oral/outercourse axis compatibility filters. |
|
| `row_item.py` | Row item selection, weighted item/pair choice, item-template axis filling, and oral/outercourse/anal axis compatibility filters. |
|
||||||
|
| `outercourse_action_policy.py` | Shared boobjob, testicle-sucking, penis-licking, handjob, and footjob action-kind classification used by row selection, role graphs, and Krea detail routing. |
|
||||||
| `row_category_route.py` | Row category/subcategory/item route resolution behind `CategoryItemRoute`, hardcore position-category filtering, cast-count adjustment, pose-vs-content seed-axis choice, item metadata collection, legacy dict compatibility, and pose-category item sanitizing. |
|
| `row_category_route.py` | Row category/subcategory/item route resolution behind `CategoryItemRoute`, hardcore position-category filtering, cast-count adjustment, pose-vs-content seed-axis choice, item metadata collection, legacy dict compatibility, and pose-category item sanitizing. |
|
||||||
| `row_rendering.py` | Row prompt/caption text-field resolution, template selection, safe formatting, default prompt templates, configured-cast descriptor insertion, and POV directive insertion. |
|
| `row_rendering.py` | Row prompt/caption text-field resolution, template selection, safe formatting, default prompt templates, configured-cast descriptor insertion, and POV directive insertion. |
|
||||||
| `row_role_graph.py` | Row role-graph route sequencing, including hardcore source graph construction, pose-category environment-anchor cleanup, and POV role-graph rewriting. |
|
| `row_role_graph.py` | Row role-graph route sequencing, including hardcore source graph construction, pose-category environment-anchor cleanup, and POV role-graph rewriting. |
|
||||||
@@ -103,33 +119,40 @@ Core helper ownership:
|
|||||||
| `pair_builder.py` | Insta/OF pair route sequencing behind `InstaPairBuildRequest` and `InstaPairBuildDependencies`, including option/filter/seed/cast parsing handoff, soft/hard row, cast, camera, clothing, and final output adapter orchestration. |
|
| `pair_builder.py` | Insta/OF pair route sequencing behind `InstaPairBuildRequest` and `InstaPairBuildDependencies`, including option/filter/seed/cast parsing handoff, soft/hard row, cast, camera, clothing, and final output adapter orchestration. |
|
||||||
| `pair_rows.py` | Insta/OF soft/hard row creation behind `InstaPairRowsRoute`, softcore expression override resolution, Woman A slot context application, soft outfit/pose overrides, POV row fields, and legacy dict compatibility. |
|
| `pair_rows.py` | Insta/OF soft/hard row creation behind `InstaPairRowsRoute`, softcore expression override resolution, Woman A slot context application, soft outfit/pose overrides, POV row fields, and legacy dict compatibility. |
|
||||||
| `pair_cast.py` | Insta/OF descriptor prose, descriptor-entry assembly, shared descriptors, cast-label cleanup, same-cast softcore descriptor text, partner styling selection, cast-summary wording, platform/level labels, softcore cast presence text, and hard cast summary text. |
|
| `pair_cast.py` | Insta/OF descriptor prose, descriptor-entry assembly, shared descriptors, cast-label cleanup, same-cast softcore descriptor text, partner styling selection, cast-summary wording, platform/level labels, softcore cast presence text, and hard cast summary text. |
|
||||||
|
| `softcore_text_policy.py` | Shared softcore pair prose for solo/same-cast/POV cast presence, caption side setup wording, and creator-shot teaser style directives. |
|
||||||
| `pair_camera.py` | Insta/OF soft/hard camera route resolution behind `InstaPairCameraRoute`, same-as-softcore camera mode, camera-detail override, camera-aware composition mutation, POV camera suppression, synchronized row/root camera metadata, and legacy dict compatibility. |
|
| `pair_camera.py` | Insta/OF soft/hard camera route resolution behind `InstaPairCameraRoute`, same-as-softcore camera mode, camera-detail override, camera-aware composition mutation, POV camera suppression, synchronized row/root camera metadata, and legacy dict compatibility. |
|
||||||
| `pair_clothing.py` | Insta/OF clothing sentence formatting and hardcore clothing continuity behind `HardcorePairClothingRoute`, body-exposure scene cleanup, action-aware body-access flags, conflicting outfit-piece cleanup, configured/default visible-person clothing, final root clothing-state assembly, and legacy dict compatibility. |
|
| `pair_clothing.py` | Insta/OF clothing sentence formatting and hardcore clothing continuity behind `HardcorePairClothingRoute`, body-exposure scene cleanup, action-aware body-access flags, conflicting outfit-piece cleanup, configured/default visible-person clothing, final root clothing-state assembly, and legacy dict compatibility. |
|
||||||
| `pair_output.py` | Insta/OF final pair prompts, trigger preservation, negative prompts, captions, and root pair metadata assembly. |
|
| `pair_output.py` | Insta/OF final pair prompts, trigger preservation, negative prompts, captions, and root pair metadata assembly. |
|
||||||
|
| `krea_format_route.py` | Top-level Krea dispatch, option normalization, metadata-vs-text input selection, single-vs-pair branching, extra positive/negative merging, final prose hygiene, and output shape. |
|
||||||
| `hardcore_role_graphs.py` | Source role graph construction for hardcore configured-cast rows, including POV-aware interaction geometry, called through `row_role_graph.py` for row generation. |
|
| `hardcore_role_graphs.py` | Source role graph construction for hardcore configured-cast rows, including POV-aware interaction geometry, called through `row_role_graph.py` for row generation. |
|
||||||
| `hardcore_role_fallback.py` | Solo, same-sex, mixed group fallback, and support-partner role graph wording for configured casts. |
|
| `hardcore_role_fallback.py` | Solo, same-sex, mixed group fallback, and support-partner role graph wording for configured casts. |
|
||||||
| `hardcore_role_interaction.py` | Foreplay, manual stimulation, body worship, clothing transition, dominant guidance, camera performance, aftercare, and group coordination role graph wording. |
|
| `hardcore_role_interaction.py` | Foreplay, manual stimulation, body worship, clothing transition, dominant guidance, camera performance, aftercare, and group coordination role graph wording. |
|
||||||
| `hardcore_role_oral.py` | Oral-sex role graph wording for kneeling, face-sitting, sixty-nine, edge-supported, side-lying, chair, standing, and reclining oral geometry. |
|
| `hardcore_role_oral.py` | Oral-sex role graph wording for kneeling, face-sitting, sixty-nine, edge-supported, side-lying, chair, standing, and reclining oral geometry. |
|
||||||
| `hardcore_role_outercourse.py` | Outercourse role graph wording for boobjob, testicle-sucking, penis-licking, handjob, and footjob geometry. |
|
| `hardcore_role_outercourse.py` | Outercourse role graph wording for boobjob, testicle-sucking, penis-licking, handjob, and footjob geometry, keyed by `outercourse_action_policy.py`. |
|
||||||
| `hardcore_role_penetration.py` | Penetrative-sex role graph wording for missionary, cowgirl, reverse-cowgirl, doggy, standing, side-lying, raised-edge, kneeling-straddle, and lotus geometry. |
|
| `hardcore_role_penetration.py` | Penetrative-sex role graph wording for missionary, cowgirl, reverse-cowgirl, doggy, standing, side-lying, raised-edge, kneeling-straddle, and lotus geometry. |
|
||||||
| `hardcore_role_anal.py` | Anal and double-contact role graph wording for rear-entry, raised-edge, kneeling, side-lying, and front/back double-position geometry. |
|
| `hardcore_role_anal.py` | Anal and double-contact role graph wording for rear-entry, raised-edge, kneeling, side-lying, and front/back double-position geometry. |
|
||||||
| `hardcore_role_climax.py` | Climax and ejaculation aftermath role graph wording for face/body/ass, lap, open-thigh, side-lying, and group front/back placement. |
|
| `hardcore_role_climax.py` | Climax and ejaculation aftermath role graph wording for face/body/ass, lap, open-thigh, side-lying, and group front/back placement. |
|
||||||
| `hardcore_action_metadata.py` | Source action-family and position-family metadata used by Krea2, SDXL, and caption routes. |
|
| `hardcore_action_metadata.py` | Source action-family and position-family metadata used by Krea2, SDXL, and caption routes. |
|
||||||
| `route_metadata.py` | Shared row-level route metadata readers for normalized action family, position family/keys, and formatter hints used by Krea2, SDXL, and caption routes. |
|
| `route_metadata.py` | Shared row-level route metadata readers for normalized action family, position family/keys, and formatter hints used by Krea2, SDXL, and caption routes. |
|
||||||
| `pov_policy.py` | Shared POV slot detection, POV label merging/filtering, builder POV directives, source role-graph viewer replacement, and shared POV composition cleanup used by builder and Krea2 routes. |
|
| `pov_policy.py` | Shared POV slot detection, POV label merging/filtering, builder POV directives, source role-graph viewer replacement, and shared POV composition cleanup used by builder and Krea2 routes. |
|
||||||
| `scene_camera_adapters.py` | Location-aware camera/scene prose such as coworking lounge camera layout. |
|
| `scene_camera_adapters.py` | Location-aware camera/scene prose for coworking, library, private creator, mirror/bedroom/studio/bathroom, and semi-public corridor/garage/archive/etc. profiles, metadata-first profile resolution, and camera-aware composition cleanup. |
|
||||||
| `row_camera.py` | Row-level camera insertion, contextual coworking composition mutation, subject-kind detection, POV label fallback, and POV suppression of normal camera directives. |
|
| `row_camera.py` | Row-level camera insertion, contextual scene composition mutation, subject-kind detection, POV label fallback, and POV suppression of normal camera directives. |
|
||||||
| `krea_row_fields.py` | Shared Krea normal-row field extraction for item, scene, pose, expression, composition/source-composition, camera, and style used by normal and configured-cast routes. |
|
| `krea_row_fields.py` | Shared Krea normal-row field extraction for item, scene, pose, expression, composition/source-composition, camera, and style used by normal and configured-cast routes. |
|
||||||
| `krea_cast.py` | Shared formatter cast descriptor parsing, cast labels, cast prose, natural cast descriptor text, and label replacement used by Krea2 and caption routes. |
|
| `krea_cast.py` | Shared formatter cast descriptor parsing, cast labels, cast prose, natural cast descriptor text, and label replacement used by Krea2 and caption routes. |
|
||||||
| `prompt_hygiene.py` | Generic prompt, caption, and negative-prompt cleanup. |
|
| `prompt_hygiene.py` | Generic prompt, caption, and negative-prompt cleanup, including route-agnostic negative-prompt merge/dedupe. |
|
||||||
| `row_normalization.py` | Final prompt-row and pair metadata normalization: trigger prepending, extra-positive append, negative merge/dedupe, caption-part joining, embedded soft/hard row output and side-metadata synchronization, and embedded row sanitation. |
|
| `row_normalization.py` | Final prompt-row and pair metadata normalization: legacy built-in subject/count/scene/appearance/item/pose/expression metadata enrichment, trigger prepending, extra-positive append, negative merge/dedupe, caption-part joining, embedded soft/hard row output and side-metadata synchronization, and embedded row sanitation. |
|
||||||
|
| `formatter_detail.py` | Shared formatter detail-level choices, normalization, and concise/balanced/dense gates used by Krea2 and caption routes. |
|
||||||
| `formatter_input.py` | Shared formatter input parsing: text cleanup, metadata/source JSON detection, trigger-prefix stripping, shared prompt field-label inventory, fallback field-label stripping, `Avoid:` splitting, prompt-field extraction, and metadata row-value fallback. |
|
| `formatter_input.py` | Shared formatter input parsing: text cleanup, metadata/source JSON detection, trigger-prefix stripping, shared prompt field-label inventory, fallback field-label stripping, `Avoid:` splitting, prompt-field extraction, and metadata row-value fallback. |
|
||||||
|
| `formatter_target.py` | Shared formatter target choices and normalization for `auto`, `single`, `softcore`, and `hardcore`, including pair-side selection and combined-caption inclusion policy. |
|
||||||
| `node_tooltips.py` | Node input tooltip inventory, node-specific overrides, dynamic-input fallback rules, and tooltip injection installer used by `__init__.py`. |
|
| `node_tooltips.py` | Node input tooltip inventory, node-specific overrides, dynamic-input fallback rules, and tooltip injection installer used by `__init__.py`. |
|
||||||
| `server_routes.py` | Pure payload handlers for profile-save and accumulator server endpoints, used by ComfyUI routes and smoke tests without importing ComfyUI. |
|
| `server_routes.py` | Pure payload handlers for profile-save and accumulator server endpoints, used by ComfyUI routes and smoke tests without importing ComfyUI. |
|
||||||
| `sdxl_presets.py` | SDXL formatter profiles, style presets, quality presets, default negative prompt, and metadata-family tag hints used by the SDXL formatter and node choice lists. |
|
| `sdxl_presets.py` | SDXL formatter profiles, style presets, quality presets, default negative prompt, and metadata-family tag hints used by the SDXL formatter and node choice lists. |
|
||||||
| `sdxl_tag_policy.py` | SDXL tag splitting, tag-key dedupe, count inference, character descriptor tags, metadata-family/camera/explicit helper tags, and route dependency assembly used by `sdxl_formatter.py` and `sdxl_tag_routes.py`. |
|
| `sdxl_format_route.py` | Top-level SDXL dispatch, formatter profile application, target and nude-weight normalization, metadata-vs-text input selection, single-vs-pair branching, final prompt/negative output shape, and fallback routing. |
|
||||||
|
| `sdxl_tag_policy.py` | SDXL tag splitting, tag-key dedupe, count inference, character descriptor tags, item-axis tags, metadata-family/camera/explicit helper tags, and route dependency assembly used by `sdxl_formatter.py` and `sdxl_tag_routes.py`. |
|
||||||
|
| `caption_format_route.py` | Top-level caption dispatch, input-hint and target normalization, caption profile application, metadata-vs-text branching, trigger wrapping, final prose hygiene, and method/output shape. |
|
||||||
| `caption_policy.py` | Caption naturalizer policy data and helpers: caption profiles, style tails, item labels, metadata-family caption labels, detail/style-policy normalization, clothing cleanup, and composition cleanup. |
|
| `caption_policy.py` | Caption naturalizer policy data and helpers: caption profiles, style tails, item labels, metadata-family caption labels, detail/style-policy normalization, clothing cleanup, and composition cleanup. |
|
||||||
| `caption_text_policy.py` | Caption sentence helpers, trigger wrapping, formatter-hint append, row-value fallback wrappers, cast text wrappers, single-caption front parsing, and metadata-route dependency assembly used by `caption_naturalizer.py` and `caption_metadata_routes.py`. |
|
| `caption_text_policy.py` | Caption sentence helpers, trigger wrapping, formatter-hint append, item-axis detail prose, row-value fallback wrappers, cast text wrappers, single-caption front parsing, and metadata-route dependency assembly used by `caption_naturalizer.py` and `caption_metadata_routes.py`. |
|
||||||
|
| `item_axis_policy.py` | Shared `item_axis_values` flattening, placeholder filtering, preferred dict-value extraction, priority-ordered Krea action context text, and row-axis text extraction used by Krea2, SDXL, and caption routes. |
|
||||||
|
|
||||||
## Node IO Map
|
## Node IO Map
|
||||||
|
|
||||||
@@ -143,9 +166,9 @@ recoverable.
|
|||||||
| `SxCP Prompt Builder` | category, subcategory, seed, optional config nodes | `prompt`, `negative_prompt`, `caption`, `metadata_json`, `category`, `subcategory` |
|
| `SxCP Prompt Builder` | category, subcategory, seed, optional config nodes | `prompt`, `negative_prompt`, `caption`, `metadata_json`, `category`, `subcategory` |
|
||||||
| `SxCP Prompt Builder From Configs` | category/cast/profile/filter/config node outputs | Same as `SxCP Prompt Builder` |
|
| `SxCP Prompt Builder From Configs` | category/cast/profile/filter/config node outputs | Same as `SxCP Prompt Builder` |
|
||||||
| `SxCP Insta/OF Prompt Pair` | options, seed_config, character_cast, location/composition/camera, hardcore_position_config | `softcore_prompt`, `hardcore_prompt`, both negatives, both captions, `shared_descriptor`, `metadata_json` |
|
| `SxCP Insta/OF Prompt Pair` | options, seed_config, character_cast, location/composition/camera, hardcore_position_config | `softcore_prompt`, `hardcore_prompt`, both negatives, both captions, `shared_descriptor`, `metadata_json` |
|
||||||
| `SxCP Krea2 Formatter` | `source_text`, optional `metadata_json`, target | `krea_prompt`, both pair prompts if pair metadata exists, negative outputs, method |
|
| `SxCP Krea2 Formatter` | `source_text`, connectable `metadata_json`, target | `krea_prompt`, both pair prompts if pair metadata exists, negative outputs, method, `route_trace_json` |
|
||||||
| `SxCP SDXL Formatter` | `source_text`, optional `metadata_json`, target, style/quality preset | `sdxl_prompt`, both pair prompts if pair metadata exists, negative outputs, method |
|
| `SxCP SDXL Formatter` | `source_text`, connectable `metadata_json`, target, style/quality preset | `sdxl_prompt`, both pair prompts if pair metadata exists, negative outputs, method, `route_trace_json` |
|
||||||
| `SxCP Caption Naturalizer` | `source_text`, optional `metadata_json` | `natural_caption`, method |
|
| `SxCP Caption Naturalizer` | `source_text`, connectable `metadata_json`, target | `natural_caption`, method, `route_trace_json` |
|
||||||
|
|
||||||
## Practical Recipes
|
## Practical Recipes
|
||||||
|
|
||||||
@@ -165,6 +188,7 @@ These recipes identify the intended road before editing prompt text.
|
|||||||
| Use Qwen/orbit camera geometry | Qwen/orbit node -> camera_config -> builder/pair | For pair, use `softcore_camera_config` and/or `hardcore_camera_config`; set mode from config in options | `_camera_config_with_mode`, `_camera_directive`, `_camera_scene_directive_for_context` |
|
| Use Qwen/orbit camera geometry | Qwen/orbit node -> camera_config -> builder/pair | For pair, use `softcore_camera_config` and/or `hardcore_camera_config`; set mode from config in options | `_camera_config_with_mode`, `_camera_directive`, `_camera_scene_directive_for_context` |
|
||||||
| Use Krea2 for only hard prompt from a pair | Pair `metadata_json` -> Krea2 Formatter | `target=hardcore`, `input_hint=metadata_json` or auto with metadata connected | `_insta_pair_to_krea`, hard row fields |
|
| Use Krea2 for only hard prompt from a pair | Pair `metadata_json` -> Krea2 Formatter | `target=hardcore`, `input_hint=metadata_json` or auto with metadata connected | `_insta_pair_to_krea`, hard row fields |
|
||||||
| Convert builder output to SDXL tags | Builder/pair metadata -> SDXL Formatter | Use metadata input; set `target`; select style and quality preset | `sdxl_tag_routes.py`, `sdxl_tag_policy.py`, compatibility wrappers `_row_core_tags` / `_soft_tags` / `_hard_tags` |
|
| Convert builder output to SDXL tags | Builder/pair metadata -> SDXL Formatter | Use metadata input; set `target`; select style and quality preset | `sdxl_tag_routes.py`, `sdxl_tag_policy.py`, compatibility wrappers `_row_core_tags` / `_soft_tags` / `_hard_tags` |
|
||||||
|
| Convert pair metadata to one training caption side | Pair `metadata_json` -> Caption Naturalizer | `target=softcore` or `target=hardcore`; use `training_concise` or `training_dense` as needed | `caption_format_route.py`, `caption_metadata_routes.insta_of_pair_from_row_result` |
|
||||||
| Save/reuse character | Slot/profile nodes -> Profile Save/Load -> slot/builder | Save from the row/profile data you want, not a freshly randomized disconnected route | `character_profile.py`, `web/profile_buttons.js`, profile JSON |
|
| Save/reuse character | Slot/profile nodes -> Profile Save/Load -> slot/builder | Save from the row/profile data you want, not a freshly randomized disconnected route | `character_profile.py`, `web/profile_buttons.js`, profile JSON |
|
||||||
|
|
||||||
## Seed Axes
|
## Seed Axes
|
||||||
@@ -187,7 +211,10 @@ Seed routing is centralized in `seed_config.py` around `SEED_AXIS_SALTS`,
|
|||||||
`SxCP Global Seed`, `SxCP Seed Control`, and `SxCP Seed Locker` all feed
|
`SxCP Global Seed`, `SxCP Seed Control`, and `SxCP Seed Locker` all feed
|
||||||
`seed_config`. Values below zero mean the row's main seed still drives that
|
`seed_config`. Values below zero mean the row's main seed still drives that
|
||||||
axis. Fixed axis seeds allow changing only one road, for example changing
|
axis. Fixed axis seeds allow changing only one road, for example changing
|
||||||
`pose`/`role` while keeping person, scene, and category stable.
|
`pose`/`role` while keeping person, scene, and category stable. `SxCP Seed
|
||||||
|
Control` keeps `seed_config` as its first output and also emits a `summary`
|
||||||
|
showing resolved per-axis values, including random-mode seeds whose widget value
|
||||||
|
may still be `-1`.
|
||||||
|
|
||||||
## Seed Playbook
|
## Seed Playbook
|
||||||
|
|
||||||
@@ -210,6 +237,26 @@ Common trap: `row_number` participates in `seed_config.axis_rng`. If two
|
|||||||
workflows have the same seeds but different `row_number`, they are not expected
|
workflows have the same seeds but different `row_number`, they are not expected
|
||||||
to match.
|
to match.
|
||||||
|
|
||||||
|
Character `slot_seed` is more specific than the `person` axis. A fixed
|
||||||
|
`slot_seed` owns that slot's random age/body/appearance/hair choices, so
|
||||||
|
rerolling `person_seed` will not drift that character. Leave `slot_seed=-1`
|
||||||
|
when the slot should follow `person_seed` or the global seed like the fallback
|
||||||
|
cast generator.
|
||||||
|
|
||||||
|
Each generated row stores `generation_trace.seed_axes` in `metadata_json`.
|
||||||
|
Use it to verify whether an axis followed the main seed or a configured seed,
|
||||||
|
and to compare the exact per-axis RNG seed used for the row.
|
||||||
|
|
||||||
|
`tools/prompt_map_audit.py` includes a runtime metadata route check. It builds a
|
||||||
|
representative single row and Insta/OF pair, verifies embedded
|
||||||
|
`generation_trace` fields, and confirms Krea2, SDXL, and caption formatters
|
||||||
|
consume metadata JSON instead of silently falling back to raw prompt text. The
|
||||||
|
formatter route traces also expose selected row metadata such as selected pair
|
||||||
|
side, category, action/position family, scene profile, position keys, and POV
|
||||||
|
labels.
|
||||||
|
The same audit also statically rejects direct `row["prompt"]` reads in
|
||||||
|
formatter metadata modules outside the shared fallback helpers.
|
||||||
|
|
||||||
## Category Sources
|
## Category Sources
|
||||||
|
|
||||||
There are two category systems.
|
There are two category systems.
|
||||||
@@ -222,6 +269,13 @@ There are two category systems.
|
|||||||
JSON categories are the scalable system. Add new main categories or subcategories
|
JSON categories are the scalable system. Add new main categories or subcategories
|
||||||
there unless the behavior needs Python logic.
|
there unless the behavior needs Python logic.
|
||||||
|
|
||||||
|
Exact JSON subcategory selection uses the full selector returned by
|
||||||
|
`category_library.exact_subcategory_selector`: `Category name / Subcategory
|
||||||
|
name`. The resolver parses that selector against the loaded category library
|
||||||
|
instead of blindly splitting the first slash separator, so custom category or
|
||||||
|
subcategory names may themselves contain `/` without drifting to a sibling
|
||||||
|
route.
|
||||||
|
|
||||||
## JSON Category Road
|
## JSON Category Road
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
@@ -255,6 +309,12 @@ Important JSON keys:
|
|||||||
map keyed by `krea`, `sdxl`, or `caption`; aliases such as `krea2` and
|
map keyed by `krea`, `sdxl`, or `caption`; aliases such as `krea2` and
|
||||||
`training_caption` are normalized by `category_template_metadata.py` and
|
`training_caption` are normalized by `category_template_metadata.py` and
|
||||||
consumed only by the matching formatter route plus the shared `all` route.
|
consumed only by the matching formatter route plus the shared `all` route.
|
||||||
|
- `item_template_metadata`: optional default route metadata on a category,
|
||||||
|
subcategory, or item. String templates inherit it, and object templates can
|
||||||
|
override it while formatter hints merge.
|
||||||
|
- For mixed hardcore subcategories, `action_family: default` keeps the explicit
|
||||||
|
position family while allowing `row_route_metadata.py` to infer the semantic
|
||||||
|
action family from the selected action/role text.
|
||||||
- `axes`: values used to fill `item_templates`.
|
- `axes`: values used to fill `item_templates`.
|
||||||
- `scene_pool` / `scene_pools` or direct `scenes`: location road.
|
- `scene_pool` / `scene_pools` or direct `scenes`: location road.
|
||||||
- `expression_pool` / `expression_pools` or direct `expressions`: expression road.
|
- `expression_pool` / `expression_pools` or direct `expressions`: expression road.
|
||||||
@@ -316,6 +376,10 @@ Edit targets:
|
|||||||
- Add reusable named locations: `categories/location_pools.json`.
|
- Add reusable named locations: `categories/location_pools.json`.
|
||||||
- Add category-specific locations: the category JSON file.
|
- Add category-specific locations: the category JSON file.
|
||||||
- Add quick workflow-only locations: `SxCP Location Pool` custom locations.
|
- Add quick workflow-only locations: `SxCP Location Pool` custom locations.
|
||||||
|
Plain text, `slug: text`, one-line JSON objects, and one-line JSON arrays are
|
||||||
|
supported; JSON entries preserve metadata such as inline `camera_profile`.
|
||||||
|
- Add quick workflow-only compositions: `SxCP Composition Pool` custom
|
||||||
|
compositions. Plain text and one-line JSON objects/arrays are supported.
|
||||||
- Add themed location packs: `THEMATIC_LOCATION_PRESETS` in `location_config.py`.
|
- Add themed location packs: `THEMATIC_LOCATION_PRESETS` in `location_config.py`.
|
||||||
|
|
||||||
### Expression
|
### Expression
|
||||||
@@ -485,13 +549,14 @@ plain prompt text. When debugging, inspect these fields before editing pools.
|
|||||||
| Field | Owner | Consumed by | Meaning |
|
| Field | Owner | Consumed by | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `source` | `build_prompt` / row builder | All formatters | Usually `json_category` or `built_in_generator`; tells which route created the row. |
|
| `source` | `build_prompt` / row builder | All formatters | Usually `json_category` or `built_in_generator`; tells which route created the row. |
|
||||||
|
| `generation_trace` | `builder_prompt_route.build_prompt_result` | Debug | Compact generation route trace containing builder branch, input/resolved category, row seed, per-axis seed sources/RNG seeds, effective clothing/pose/figure choices, expression route, and content seed axis. |
|
||||||
| `main_category`, `subcategory` | `row_category_route.select_category_item_route` | All formatters and debug | Human-readable selected category route. |
|
| `main_category`, `subcategory` | `row_category_route.select_category_item_route` | All formatters and debug | Human-readable selected category route. |
|
||||||
| `category_slug`, `subcategory_slug` | `row_category_route.select_category_item_route` | Debug/filtering | Stable-ish machine labels for selected category route. |
|
| `category_slug`, `subcategory_slug` | `row_category_route.select_category_item_route` | Debug/filtering | Stable-ish machine labels for selected category route. |
|
||||||
| `content_seed_axis` | `row_category_route.select_category_item_route` | Debug | Shows whether the item/action was driven by `content` or `pose`. Critical for hardcore pose categories. |
|
| `content_seed_axis` | `row_category_route.select_category_item_route` | Debug | Shows whether the item/action was driven by `content` or `pose`. Critical for hardcore pose categories. |
|
||||||
| `item` | `row_category_route.select_category_item_route` or Insta override | Krea/SDXL/Naturalizer | Clothing item, category item, or sexual scene/action text. |
|
| `item` | `row_category_route.select_category_item_route` or Insta override | Krea/SDXL/Naturalizer | Clothing item, category item, or sexual scene/action text. |
|
||||||
| `item_axis_values` | `row_category_route.select_category_item_route` | Krea hardcore rewrite, SDXL tags | Filled template axes such as position/action/detail values. |
|
| `item_axis_values` | `row_category_route.select_category_item_route` | Krea hardcore rewrite, SDXL tags, natural captions | Filled template axes such as position/action/detail values. Shared flattening lives in `item_axis_policy.py`. |
|
||||||
| `item_template_metadata` | `row_category_route.select_category_item_route` | Debug, Krea/SDXL/Naturalizer route metadata | Optional metadata from object-style item templates; currently used to prefer explicit action/position families and keys before inference. |
|
| `item_template_metadata` | `row_category_route.select_category_item_route` | Debug, Krea/SDXL/Naturalizer route metadata | Metadata inherited from category/subcategory/item `item_template_metadata` plus selected object-template metadata; used to prefer explicit action/position families and keys before inference. |
|
||||||
| `formatter_hints` | `row_category_route.select_category_item_route` | Krea/SDXL/Naturalizer route specialization, debug | Normalized route-specific hints from object-style item templates, keyed by `all`, `krea`, `sdxl`, or `caption`; each formatter consumes `all` plus its own route only. |
|
| `formatter_hints` | `row_category_route.select_category_item_route` | Krea/SDXL/Naturalizer route specialization, debug | Normalized route-specific hints inherited from template metadata, keyed by `all`, `krea`, `sdxl`, or `caption`; each formatter consumes `all` plus its own route only. |
|
||||||
| `action_family` | `row_route_metadata.resolve_action_position_route` | Krea hardcore rewrite, SDXL tags, natural captions, debug | Source-aware formatter semantic family such as `foreplay`, `outercourse`, `oral`, `penetration`, `toy_double`, or `climax`. |
|
| `action_family` | `row_route_metadata.resolve_action_position_route` | Krea hardcore rewrite, SDXL tags, natural captions, debug | Source-aware formatter semantic family such as `foreplay`, `outercourse`, `oral`, `penetration`, `toy_double`, or `climax`. |
|
||||||
| `position_family` | `row_route_metadata.resolve_action_position_route` | Debug/filtering | Source/UI hardcore family selected by template metadata or subcategory, such as `manual`, `interaction`, `oral`, `anal`, or `climax`. |
|
| `position_family` | `row_route_metadata.resolve_action_position_route` | Debug/filtering | Source/UI hardcore family selected by template metadata or subcategory, such as `manual`, `interaction`, `oral`, `anal`, or `climax`. |
|
||||||
| `position_key`, `position_keys` | `row_route_metadata.resolve_action_position_route` | Debug/future filters | Concrete position tokens from object-template metadata and inferred axes/role text, such as `kneeling`, `doggy`, `boobjob`, or `open_thighs`. |
|
| `position_key`, `position_keys` | `row_route_metadata.resolve_action_position_route` | Debug/future filters | Concrete position tokens from object-template metadata and inferred axes/role text, such as `kneeling`, `doggy`, `boobjob`, or `open_thighs`. |
|
||||||
@@ -499,6 +564,8 @@ plain prompt text. When debugging, inspect these fields before editing pools.
|
|||||||
| `role_graph` | `_role_graph`, POV adapter | Krea/Naturalizer | Choreography/action relationship text after POV adaptation. |
|
| `role_graph` | `_role_graph`, POV adapter | Krea/Naturalizer | Choreography/action relationship text after POV adaptation. |
|
||||||
| `source_role_graph` | `_role_graph` before POV rewrite | Krea hardcore rewrite | Raw action graph used to infer position and contact. |
|
| `source_role_graph` | `_role_graph` before POV rewrite | Krea hardcore rewrite | Raw action graph used to infer position and contact. |
|
||||||
| `scene_text` | `row_prompt_axes.resolve_prompt_axes` | All formatters | Final location text. |
|
| `scene_text` | `row_prompt_axes.resolve_prompt_axes` | All formatters | Final location text. |
|
||||||
|
| `scene_entry` | `row_prompt_axes.resolve_prompt_axes` / `row_location` | Debug/future route rules | Structured selected scene entry, preserving slug/prompt plus theme metadata when available. |
|
||||||
|
| `location_theme`, `scene_theme` | `location_config.py`, selected scene entry | Debug/camera route rules | Active theme on the location config and theme of the selected scene. This makes theme-driven behavior inspectable instead of only string-inferred. |
|
||||||
| `source_scene_text` | location/body-exposure/camera adapters | Debug/continuity | Previous scene text before an override. |
|
| `source_scene_text` | location/body-exposure/camera adapters | Debug/continuity | Previous scene text before an override. |
|
||||||
| `location_config` | Location config parser | Debug | Active location pool config, if connected. |
|
| `location_config` | Location config parser | Debug | Active location pool config, if connected. |
|
||||||
| `pose` | `row_prompt_axes.resolve_prompt_axes` | Formatters | Generic pose text. Less important for hardcore action categories than `item`/`role_graph`. |
|
| `pose` | `row_prompt_axes.resolve_prompt_axes` | Formatters | Generic pose text. Less important for hardcore action categories than `item`/`role_graph`. |
|
||||||
@@ -508,11 +575,13 @@ plain prompt text. When debugging, inspect these fields before editing pools.
|
|||||||
| `expression_enabled`, `expression_disabled` | Builder/slot override | All formatters | Hard gate for whether expression text should appear. |
|
| `expression_enabled`, `expression_disabled` | Builder/slot override | All formatters | Hard gate for whether expression text should appear. |
|
||||||
| `expression_intensity_source` | Builder/slot override | Debug | Explains whether intensity came from input, random, slot, or disabled state. |
|
| `expression_intensity_source` | Builder/slot override | Debug | Explains whether intensity came from input, random, slot, or disabled state. |
|
||||||
| `composition` | `row_prompt_axes.resolve_prompt_axes` | All formatters | Final framing phrase. |
|
| `composition` | `row_prompt_axes.resolve_prompt_axes` | All formatters | Final framing phrase. |
|
||||||
|
| `composition_entry`, `composition_theme` | `row_prompt_axes.resolve_prompt_axes` / `row_location` | Debug/future route rules | Structured selected composition entry and active composition theme. |
|
||||||
| `source_composition` | `row_prompt_axes.resolve_prompt_axes` | Krea hardcore rewrite | Previous/raw composition, often better for action inference. |
|
| `source_composition` | `row_prompt_axes.resolve_prompt_axes` | Krea hardcore rewrite | Previous/raw composition, often better for action inference. |
|
||||||
| `composition_config` | Composition config parser | Debug | Active composition pool config, if connected. |
|
| `composition_config` | Composition config parser | Debug | Active composition pool config, if connected. |
|
||||||
| `camera_config` | Camera nodes/parser | Krea/SDXL/debug | Structured camera settings. |
|
| `camera_config` | Camera nodes/parser | Krea/SDXL/debug | Structured camera settings. |
|
||||||
| `camera_directive` | `_camera_directive` | Krea/Naturalizer/prompt text | Human camera sentence. Suppressed for POV. |
|
| `camera_directive` | `_camera_directive` | Krea/Naturalizer/prompt text | Human camera sentence. Suppressed for POV. |
|
||||||
| `camera_scene_directive` | scene-camera adapter | Krea/Naturalizer/prompt text | Location-aware camera layout sentence. |
|
| `camera_scene_directive` | scene-camera adapter | Krea/Naturalizer/prompt text | Location-aware camera layout sentence. |
|
||||||
|
| `scene_camera_profile`, `scene_camera_profile_key` | `row_camera.apply_camera_config` | Debug/camera route rules | Structured camera profile selected for the current scene, e.g. `classical_library` or `coworking_lounge`. |
|
||||||
| `subject_type`, `subject_phrase` | `row_subject_route.resolve_subject_route` | Formatters | Single/couple/group/configured cast route. |
|
| `subject_type`, `subject_phrase` | `row_subject_route.resolve_subject_route` | Formatters | Single/couple/group/configured cast route. |
|
||||||
| `women_count`, `men_count`, `person_count` | `row_subject_route.resolve_subject_route` | Pair/formatters/debug | Effective cast counts. |
|
| `women_count`, `men_count`, `person_count` | `row_subject_route.resolve_subject_route` | Pair/formatters/debug | Effective cast counts. |
|
||||||
| `cast_descriptors`, `cast_descriptor_text` | `row_subject_route.resolve_subject_route` | Krea/SDXL/Naturalizer | Visible cast descriptors. |
|
| `cast_descriptors`, `cast_descriptor_text` | `row_subject_route.resolve_subject_route` | Krea/SDXL/Naturalizer | Visible cast descriptors. |
|
||||||
@@ -531,7 +600,7 @@ plain prompt text. When debugging, inspect these fields before editing pools.
|
|||||||
| `options` | `SxCP Insta/OF Options` | Formatters/debug | Soft/hard level, cast mode, continuity, camera modes, expression settings. |
|
| `options` | `SxCP Insta/OF Options` | Formatters/debug | Soft/hard level, cast mode, continuity, camera modes, expression settings. |
|
||||||
| `shared_descriptor` | `pair_cast.py` | Pair formatters | Primary creator descriptor. |
|
| `shared_descriptor` | `pair_cast.py` | Pair formatters | Primary creator descriptor. |
|
||||||
| `shared_cast_descriptors` | `pair_cast.py` | Pair formatters | Full cast descriptor list. |
|
| `shared_cast_descriptors` | `pair_cast.py` | Pair formatters | Full cast descriptor list. |
|
||||||
| `softcore_row`, `hardcore_row` | Pair route | Pair formatters | Full normal metadata rows for each side; their prompt, caption, negative, and side-specific metadata fields are synchronized to the final pair outputs/root fields during pair normalization. |
|
| `softcore_row`, `hardcore_row` | Pair route | Pair formatters | Full normal metadata rows for each side; their prompt, caption, negative, shared cast descriptors, and side-specific metadata fields are synchronized to the final pair outputs/root fields during pair normalization. |
|
||||||
| `softcore_prompt`, `hardcore_prompt` | `pair_output.py` | Direct output/fallback | Raw pair prompts before formatter rewrite. |
|
| `softcore_prompt`, `hardcore_prompt` | `pair_output.py` | Direct output/fallback | Raw pair prompts before formatter rewrite. |
|
||||||
| `softcore_negative_prompt`, `hardcore_negative_prompt` | `pair_output.py` | Formatter negatives | Separate negatives for each side. |
|
| `softcore_negative_prompt`, `hardcore_negative_prompt` | `pair_output.py` | Formatter negatives | Separate negatives for each side. |
|
||||||
| `softcore_partner_styling` | `pair_cast.py` | Krea/SDXL pair branch | Partner softcore clothing and pose when same-cast softcore is enabled. |
|
| `softcore_partner_styling` | `pair_cast.py` | Krea/SDXL pair branch | Partner softcore clothing and pose when same-cast softcore is enabled. |
|
||||||
@@ -639,20 +708,53 @@ Camera handling:
|
|||||||
|
|
||||||
Current camera-aware scene adapter:
|
Current camera-aware scene adapter:
|
||||||
|
|
||||||
- Coworking/business-cafe/office scenes are detected by `_is_coworking_scene`.
|
- Scene profiles live in `scene_camera_adapters.SCENE_CAMERA_PROFILES`.
|
||||||
- Location profile comes from `_coworking_location_profile`.
|
- Profile resolution is metadata-first: explicit `scene_camera_profile_key`,
|
||||||
- Direction, distance, and elevation details come from `_coworking_direction_detail`,
|
selected `scene_entry` profile keys, and theme metadata are preferred before
|
||||||
`_coworking_distance_detail`, and `_coworking_elevation_detail`.
|
text matching.
|
||||||
- Composition cleanup for coworking outfit-check wording happens in
|
- Selected scene entries can provide inline `camera_profile` or
|
||||||
`_coworking_composition_prompt`.
|
`scene_camera_profile` objects with `key`, `family`, `layout_label`, `place`,
|
||||||
|
`foreground`, `midground`, `background`, `detail_label`, and optional
|
||||||
|
per-subject `composition` text.
|
||||||
|
- Coworking/business-cafe/office scenes, classical library/book-stack scenes,
|
||||||
|
private creator/mirror/bedroom/studio/bathroom scenes, and semi-public
|
||||||
|
repeating-structure scenes such as hotel corridors, parking garages,
|
||||||
|
archives, laundromats, station lockers, backstage halls, wine cellars,
|
||||||
|
nightclub back halls, and restaurant booths are detected by
|
||||||
|
`scene_camera_profile`. Known JSON scene slugs route through
|
||||||
|
`SCENE_SLUG_PROFILE_KEYS` before text matching, which prevents broad terms
|
||||||
|
like cafe, mirror, tiled walls, or bookshelves from hijacking unrelated
|
||||||
|
locations.
|
||||||
|
- Location themes preserve `theme` on configs and selected scene entries, and
|
||||||
|
rows expose `location_theme`, `scene_theme`, `composition_theme`, and
|
||||||
|
`scene_camera_profile_key` for debugging and future route rules.
|
||||||
|
- Direction, distance, and elevation details come from profile-aware helpers
|
||||||
|
such as `scene_direction_detail`, `scene_distance_detail`, and
|
||||||
|
`scene_elevation_detail`.
|
||||||
|
- Composition cleanup for mismatched outfit-check, mirror, bag, or shoes
|
||||||
|
wording happens in `contextual_composition_prompt`; compatibility wrappers
|
||||||
|
keep the old coworking function names available.
|
||||||
|
|
||||||
Important POV rule:
|
Important POV rule:
|
||||||
|
|
||||||
- In POV rows, location anchors must stay behind, beside, or at the frame edges.
|
- In POV rows, location anchors must stay behind, beside, or at the frame edges.
|
||||||
The foreground belongs to the POV body/hands and visible partner/action.
|
The foreground belongs to the POV body/hands and visible partner/action.
|
||||||
|
- Profile `foreground` anchors such as desk edges, counters, shelves, doors, or
|
||||||
|
pillars are normal non-POV placement aids. POV camera text should not reuse
|
||||||
|
them as viewer-side objects; it should reserve the lower foreground for POV
|
||||||
|
body or hand cues.
|
||||||
|
|
||||||
## Formatter Routes
|
## Formatter Routes
|
||||||
|
|
||||||
|
Formatter metadata input is normalized by `formatter_input.py`. Pair routing is
|
||||||
|
structural: metadata with both a softcore side and a hardcore side
|
||||||
|
(`softcore_row`/`hardcore_row` or root soft/hard prompt/caption fields) is
|
||||||
|
treated as pair metadata even if the UI `mode` label is absent. Krea2, SDXL, and
|
||||||
|
caption routes share this detection to avoid hidden drift between formatter
|
||||||
|
paths. Formatter nodes expose `route_trace_json` with the formatter name,
|
||||||
|
branch, method, normalized target/input hint, selected pair side when relevant,
|
||||||
|
and route-specific controls.
|
||||||
|
|
||||||
### Krea2
|
### Krea2
|
||||||
|
|
||||||
`format_krea2_prompt` chooses between three roads:
|
`format_krea2_prompt` chooses between three roads:
|
||||||
@@ -696,10 +798,10 @@ Krea2 field consumption:
|
|||||||
| Branch | Reads most from | Key functions |
|
| Branch | Reads most from | Key functions |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Normal single/couple/generic row | `subject_type`, `item`, `pose`, `scene_text`, `expression`, `composition`, `camera_*`, style fields | `krea_normal_formatter.format_normal_row_result` |
|
| Normal single/couple/generic row | `subject_type`, `item`, `pose`, `scene_text`, `expression`, `composition`, `camera_*`, style fields | `krea_normal_formatter.format_normal_row_result` |
|
||||||
| Normal configured cast/hardcore row | `cast_descriptor_text`, `women_count`, `men_count`, `source_role_graph`, `role_graph`, `item`, `item_axis_values`, `source_composition`, `pov_character_labels` | `krea_configured_cast_formatter.format_configured_cast_result`, `krea_actions.hardcore_action_sentence`, `krea_pov_actions.pov_action_phrase` |
|
| Normal configured cast/hardcore row | `cast_descriptor_text` only for cast descriptors, `women_count`, `men_count`, `source_role_graph`, `role_graph`, `item`, `item_axis_values`, `source_composition`, `pov_character_labels` | `krea_configured_cast_formatter.format_configured_cast_result`, `krea_actions.hardcore_action_sentence`, `krea_pov_actions.pov_action_phrase` |
|
||||||
| Insta/OF pair softcore | `shared_descriptor`, `softcore_row`, `softcore_partner_styling`, options, soft camera fields | `krea_pair_formatter.format_insta_pair_result` |
|
| Insta/OF pair softcore | `shared_descriptor`, `softcore_row`, `softcore_partner_styling`, options, soft camera fields | `krea_pair_formatter.format_insta_pair_result` |
|
||||||
| Insta/OF pair hardcore | `hardcore_row`, `shared_cast_descriptors`, `hardcore_clothing_state`, `hardcore_detail_density`, hard camera fields, POV labels | `krea_pair_formatter.format_insta_pair_result`, `krea_actions.hardcore_action_sentence`, `krea_pov_actions.pov_action_phrase`, `krea_clothing.natural_clothing_state` |
|
| Insta/OF pair hardcore | `hardcore_row`, `shared_cast_descriptors`, `hardcore_clothing_state`, `hardcore_detail_density`, hard camera fields, POV labels | `krea_pair_formatter.format_insta_pair_result`, `krea_actions.hardcore_action_sentence`, `krea_pov_actions.pov_action_phrase`, `krea_clothing.natural_clothing_state` |
|
||||||
| Plain text fallback | `source_text` only | `_fallback_text_to_krea` |
|
| Plain text fallback | `source_text` only, including raw prompt labels such as `Scene:` / `Pose:` | `_fallback_text_to_krea` |
|
||||||
|
|
||||||
If metadata is connected and `method` says `text(fallback)`, the formatter did
|
If metadata is connected and `method` says `text(fallback)`, the formatter did
|
||||||
not parse metadata. That is a wiring/input-hint issue, not a prompt pool issue.
|
not parse metadata. That is a wiring/input-hint issue, not a prompt pool issue.
|
||||||
@@ -724,10 +826,10 @@ SDXL field consumption:
|
|||||||
|
|
||||||
| Branch | Reads most from | Key functions |
|
| Branch | Reads most from | Key functions |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Normal metadata | cast descriptors, age/body/skin/hair/eyes, `action_family`, `position_family`, `position_keys`, item, role graph, scene, camera config/directive | `sdxl_tag_routes.row_core_tags_result`, `sdxl_tag_policy.metadata_family_tags`, `sdxl_tag_policy.camera_tags` |
|
| Normal metadata | `cast_descriptor_text` or structured age/body/skin/hair/eyes, `action_family`, `position_family`, `position_keys`, `item_axis_values`, item, role graph, clothing/body exposure state, scene, camera config/directive | `sdxl_tag_routes.row_core_tags_result`, `sdxl_tag_policy.normal_character_tags`, `sdxl_tag_policy.metadata_family_tags`, `sdxl_tag_policy.axis_value_tags`, `sdxl_tag_policy.camera_tags` |
|
||||||
| Pair softcore | `softcore_row`, pair partner styling, root soft camera config | `sdxl_tag_routes.soft_tags_result` |
|
| Pair softcore | `softcore_row`, pair partner styling, root soft camera config | `sdxl_tag_routes.soft_tags_result` |
|
||||||
| Pair hardcore | `hardcore_row`, `action_family`, `position_family`, `position_keys`, `hardcore_clothing_state`, hard camera fields, hard prompt text | `sdxl_tag_routes.hard_tags_result`, `sdxl_tag_policy.metadata_family_tags` |
|
| Pair hardcore | `hardcore_row`, `action_family`, `position_family`, `position_keys`, role graph, item, `hardcore_clothing_state`, expression/composition, hard camera fields | `sdxl_tag_routes.hard_tags_result`, `sdxl_tag_policy.metadata_family_tags` |
|
||||||
| Text fallback | `source_text`, preserve-trigger setting, shared field-label stripping | `_fallback_text_to_sdxl` |
|
| Text fallback | `source_text`, preserve-trigger setting, shared field-label stripping, prompt labels such as `Characters:` | `_fallback_text_to_sdxl` |
|
||||||
|
|
||||||
SDXL is the right place for model trigger handling, tag ordering, weight syntax,
|
SDXL is the right place for model trigger handling, tag ordering, weight syntax,
|
||||||
quality/style preset changes, and nude-weight defaults. Do not solve those in
|
quality/style preset changes, and nude-weight defaults. Do not solve those in
|
||||||
@@ -748,8 +850,8 @@ Naturalizer field consumption:
|
|||||||
| Branch | Reads most from | Key functions |
|
| Branch | Reads most from | Key functions |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Normal single/couple/group | subject fields, age/body, item, scene, expression, composition, camera scene | `caption_metadata_routes.single_from_row_result`, `caption_metadata_routes.couple_from_row_result`, `caption_metadata_routes.group_or_layout_from_row_result` |
|
| Normal single/couple/group | subject fields, age/body, item, scene, expression, composition, camera scene | `caption_metadata_routes.single_from_row_result`, `caption_metadata_routes.couple_from_row_result`, `caption_metadata_routes.group_or_layout_from_row_result` |
|
||||||
| Configured cast/hardcore | `cast_descriptor_text`, `action_family`, `position_family`, `role_graph`, `item`, `scene_text`, expression, composition | `caption_metadata_routes.configured_cast_from_row_result`, `caption_text_policy.metadata_action_label` |
|
| Configured cast/hardcore | `cast_descriptor_text`, `action_family`, `position_family`, `role_graph`, `item`, `item_axis_values`, `scene_text`, expression, composition | `caption_metadata_routes.configured_cast_from_row_result`, `caption_text_policy.metadata_action_label`, `caption_text_policy.item_axis_detail_text` |
|
||||||
| Insta/OF pair | `softcore_row`, `hardcore_row`, pair options and continuity | `caption_metadata_routes.insta_of_pair_from_row_result` |
|
| Insta/OF pair | `softcore_row`, `hardcore_row`, pair options and continuity, target | `caption_metadata_routes.insta_of_pair_from_row_result` |
|
||||||
| Text fallback | `caption` or `prompt` text | `caption_naturalizer._text_to_prose`, with sentence helpers delegated to `caption_text_policy.py` |
|
| Text fallback | `caption` or `prompt` text | `caption_naturalizer._text_to_prose`, with sentence helpers delegated to `caption_text_policy.py` |
|
||||||
|
|
||||||
### Final Text Hygiene
|
### Final Text Hygiene
|
||||||
@@ -777,14 +879,14 @@ These do not own prompt pool wording, but they affect execution and review:
|
|||||||
|
|
||||||
| Node family | Files | Purpose |
|
| Node family | Files | Purpose |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Loop nodes | `loop_nodes.py`, `web/loop_slots.js` | While/for loop execution and carry values. |
|
| Loop nodes | `loop_nodes.py`, `web/loop_slots.js` | While/for loop execution and carry values. Includes `SxCP While Loop Start`, `SxCP While Loop End`, `SxCP For Loop Start`, `SxCP For Loop End`, `SxCP Loop Int Add`, `SxCP Loop Less Than`, and `SxCP Loop Less Than Or Equal`. |
|
||||||
| Index switch | `loop_nodes.py`, `index_switch_policy.py`, `web/index_switch_slots.js` | Multi-input to selected output, and selected input to multi-output routing. Pure index-base, missing-input, route-output, status, and lazy-input policy lives in `index_switch_policy.py`. |
|
| Index switch | `loop_nodes.py`, `index_switch_policy.py`, `web/index_switch_slots.js` | `SxCP Index Switch`: multi-input to selected output, and selected input to multi-output routing. Pure index-base, missing-input, route-output, status, and lazy-input policy lives in `index_switch_policy.py`. |
|
||||||
| Accumulator | `loop_nodes.py`, `web/accumulator_preview.js` | Stores generated values/images during workflow execution and previews/reorders/deletes them. |
|
| Accumulator | `loop_nodes.py`, `web/accumulator_preview.js` | Stores generated values/images during workflow execution and previews/reorders/deletes them. |
|
||||||
| Persistent text preview | `loop_nodes.py`, `web/preview_any_text.js` | Stores any value as text and keeps it after workflow reload. |
|
| Persistent text preview | `loop_nodes.py`, `web/preview_any_text.js` | Stores any value as text and keeps it after workflow reload. |
|
||||||
| Builder node wrappers | `node_builder.py`, imported by `__init__.py` | Direct prompt builder and config-driven prompt builder ComfyUI declarations. |
|
| Builder node wrappers | `node_builder.py`, imported by `__init__.py` | Direct prompt builder and config-driven prompt builder ComfyUI declarations. |
|
||||||
| Seed and resolution utility nodes | `node_seed_resolution.py`, imported by `__init__.py` | UI wrappers for global/per-axis seed configs via `seed_config.py`, plus SDXL/Krea width/height helpers. |
|
| Seed and resolution utility nodes | `node_seed_resolution.py`, imported by `__init__.py` | UI wrappers for global/per-axis seed configs via `seed_config.py`, plus SDXL/Krea width/height helpers. |
|
||||||
| Camera utility nodes | `node_camera.py`, imported by `__init__.py` | UI wrappers for direct camera config, orbit-to-camera config, and Qwen MultiAngle camera translation via `camera_config.py`. |
|
| Camera utility nodes | `node_camera.py`, imported by `__init__.py` | UI wrappers for direct camera config, orbit-to-camera config, and Qwen MultiAngle camera translation via `camera_config.py`. |
|
||||||
| Character utility nodes | `node_character.py`, imported by `__init__.py` | Hair, age/body/eyes/clothing pools, manual details, character slots, and profile save/load nodes. |
|
| Character utility nodes | `node_character.py`, imported by `__init__.py` | Hair, age/body/eyes/clothing pools, manual details, character slots, and profile save/load nodes. Includes `SxCP Character Age Range`, `SxCP Character Body Pool`, `SxCP Woman Body Pool`, `SxCP Man Body Pool`, `SxCP Eye Color Pool`, and `SxCP Character Clothing`. |
|
||||||
| Hardcore position utility nodes | `node_hardcore_position.py`, imported by `__init__.py` | Position-family pool and action/filter gates for hardcore routes. |
|
| Hardcore position utility nodes | `node_hardcore_position.py`, imported by `__init__.py` | Position-family pool and action/filter gates for hardcore routes. |
|
||||||
| Formatter utility nodes | `node_formatter.py`, imported by `__init__.py` | Caption naturalizer, Krea2 formatter, and SDXL formatter node wrappers. |
|
| Formatter utility nodes | `node_formatter.py`, imported by `__init__.py` | Caption naturalizer, Krea2 formatter, and SDXL formatter node wrappers. |
|
||||||
| Insta/OF utility nodes | `node_insta.py`, imported by `__init__.py` | Insta/OF option config and dual prompt-pair node wrappers. |
|
| Insta/OF utility nodes | `node_insta.py`, imported by `__init__.py` | Insta/OF option config and dual prompt-pair node wrappers. |
|
||||||
@@ -798,8 +900,11 @@ Run:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python tools/prompt_map_audit.py
|
python tools/prompt_map_audit.py
|
||||||
|
python tools/prompt_map_audit.py --quiet
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use `--quiet` when you only need pass/fail output in a focused validation pass.
|
||||||
|
|
||||||
The script does not import ComfyUI. It parses the repo and prints:
|
The script does not import ComfyUI. It parses the repo and prints:
|
||||||
|
|
||||||
- registered display node names and known return names;
|
- registered display node names and known return names;
|
||||||
@@ -808,11 +913,33 @@ The script does not import ComfyUI. It parses the repo and prints:
|
|||||||
- JSON reference validation for every `scene_pools`, `expression_pools`, and
|
- JSON reference validation for every `scene_pools`, `expression_pools`, and
|
||||||
`composition_pools` reference;
|
`composition_pools` reference;
|
||||||
- item template validation so `{placeholder}` names resolve to `item_axes`.
|
- item template validation so `{placeholder}` names resolve to `item_axes`.
|
||||||
|
- category identity validation so custom category names/slugs do not collide
|
||||||
|
with built-in selectors, category identities stay unique, and exact
|
||||||
|
subcategory selectors cannot become ambiguous.
|
||||||
|
- effective category route coverage so each normalized category path has
|
||||||
|
usable item, scene, expression, composition, and hardcore route metadata
|
||||||
|
before runtime fallbacks can hide a gap.
|
||||||
|
- registered route policy validation: registered route families have SDXL tags,
|
||||||
|
caption labels, and valid incompatibility filters before a new action or
|
||||||
|
position family can drift between formatter routes.
|
||||||
|
- location theme camera-profile validation so `Location Theme` presets and
|
||||||
|
their scene entries resolve to structured scene-camera profiles instead of
|
||||||
|
falling back to generic camera prose.
|
||||||
|
- route documentation validation so critical route modules are listed in this
|
||||||
|
map and the architecture plan, and registered in `SMOKE_CASES` by their
|
||||||
|
expected smoke cases.
|
||||||
|
- node documentation validation so every registered ComfyUI display name appears
|
||||||
|
in this route map or the README before the node can silently drift out of
|
||||||
|
user-facing docs.
|
||||||
|
- node registration validation so every concrete `SxCP...` node class in a
|
||||||
|
node module is present in that module's class mapping, has a matching display
|
||||||
|
mapping, and uses the same mapping key as the class name.
|
||||||
|
|
||||||
Use its output to spot doc drift after adding a new node or pool. If a new node
|
Use its output to spot doc drift after adding a new node or pool. If a new node
|
||||||
or pool appears there but not in this map, update the relevant route table. The
|
or pool appears there but not in this map, update the relevant route table. The
|
||||||
script exits nonzero when JSON pool references or item template axes do not
|
script exits nonzero when JSON pool references, item template axes, category
|
||||||
resolve.
|
identities, critical route docs, critical route smoke registrations, or
|
||||||
|
registered node classes/display names do not resolve.
|
||||||
|
|
||||||
## Behavioral Smoke Helper
|
## Behavioral Smoke Helper
|
||||||
|
|
||||||
@@ -824,11 +951,25 @@ continuity. Run:
|
|||||||
python tools/prompt_smoke.py
|
python tools/prompt_smoke.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To inspect or run a focused path while editing one route:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/prompt_smoke.py --list
|
||||||
|
python tools/prompt_smoke.py --case krea_format_route_policy --case sdxl_format_route_policy
|
||||||
|
python tools/prompt_smoke.py --quiet
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--quiet` when you only need pass/fail output for the selected cases or the
|
||||||
|
full suite.
|
||||||
|
|
||||||
The script does not import ComfyUI. It builds representative metadata rows and
|
The script does not import ComfyUI. It builds representative metadata rows and
|
||||||
pair metadata through the core Python APIs, then verifies:
|
pair metadata through the core Python APIs, then verifies:
|
||||||
|
|
||||||
- generated rows keep prompt, negative prompt, scene, composition, action item,
|
- generated rows keep prompt, negative prompt, scene, composition, action item,
|
||||||
and role graph metadata populated;
|
and role graph metadata populated;
|
||||||
|
- a user-added JSON category in a temporary category directory reaches category
|
||||||
|
choices, subcategory choices, prompt generation, and formatter metadata
|
||||||
|
routes without Python glue code;
|
||||||
- Krea2, SDXL, and natural caption routes use metadata instead of text fallback;
|
- Krea2, SDXL, and natural caption routes use metadata instead of text fallback;
|
||||||
- SDXL and caption trigger handling keeps one trigger;
|
- SDXL and caption trigger handling keeps one trigger;
|
||||||
- negative prompts do not duplicate comma-list items;
|
- negative prompts do not duplicate comma-list items;
|
||||||
@@ -873,6 +1014,59 @@ pair metadata through the core Python APIs, then verifies:
|
|||||||
- profile-save and accumulator endpoint payload handlers are smoke-tested
|
- profile-save and accumulator endpoint payload handlers are smoke-tested
|
||||||
without importing ComfyUI, and the reversible index switch keeps pick/input
|
without importing ComfyUI, and the reversible index switch keeps pick/input
|
||||||
and route/output behavior stable.
|
and route/output behavior stable.
|
||||||
|
- Krea2, SDXL, and caption formatter nodes are checked against their public
|
||||||
|
Python formatter APIs for metadata-driven pair inputs, so ComfyUI wrappers
|
||||||
|
cannot silently drift from route behavior.
|
||||||
|
|
||||||
|
## Route Simulation Helper
|
||||||
|
|
||||||
|
For prompt-quality edits, run the simulation helper before and after changing
|
||||||
|
wording or route selection:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/prompt_route_simulation.py --fail-on-issues
|
||||||
|
```
|
||||||
|
|
||||||
|
The script builds representative single-row and Insta/OF pair routes, formats
|
||||||
|
them through Krea2, SDXL, and training-caption paths, and reports structured
|
||||||
|
issues for:
|
||||||
|
|
||||||
|
- formatter routes falling back away from metadata;
|
||||||
|
- formatter route traces exposing selected row metadata such as category,
|
||||||
|
action/position family, selected pair side, scene profile, position keys, and
|
||||||
|
POV labels;
|
||||||
|
- raw builder labels leaking into Krea output;
|
||||||
|
- repeated cast descriptors in training-caption formatter output;
|
||||||
|
- duplicate negative-prompt comma items;
|
||||||
|
- softcore prompt noise;
|
||||||
|
- POV routes emitting third-person camera text or losing first-person wording;
|
||||||
|
- selected hardcore position filters appearing in `position_keys` but not as
|
||||||
|
the primary `position_key`;
|
||||||
|
- route-family coverage for registered action and position families, excluding
|
||||||
|
only documented special cases that have their own smoke fixtures;
|
||||||
|
- pair seed determinism for Insta/OF metadata and formatted soft/hard outputs;
|
||||||
|
- pair person, scene, expression, and composition rerolls changing only their
|
||||||
|
intended soft/hard metadata axes;
|
||||||
|
- pair content rerolls changing soft outfit/teaser content while keeping cast,
|
||||||
|
scene, hard action, and composition axes stable;
|
||||||
|
- pair pose rerolls changing hardcore action metadata while keeping cast,
|
||||||
|
scene, soft outfit, and composition axes stable;
|
||||||
|
- pose-axis rerolls changing cast/scene metadata or failing to move pose/action
|
||||||
|
metadata.
|
||||||
|
- multi-seed route sweeps that repeat the same route/noise/seed checks across
|
||||||
|
spaced seeds to catch random-pool drift hidden by a single clean seed.
|
||||||
|
|
||||||
|
The report also includes a `quality` section. This is the high-level progress
|
||||||
|
view for path cleanup: it groups route cases by target, action family, and
|
||||||
|
position family; counts route issues separately from coverage/seed-check
|
||||||
|
issues; buckets issue types such as label leaks, softcore noise, trace
|
||||||
|
mismatches, trigger drift, or reroll drift; and lists the weakest cases first
|
||||||
|
when a sweep finds failures.
|
||||||
|
|
||||||
|
Use `--json --include-prompts` when you need the exact raw and formatted text
|
||||||
|
for debugging a route. Use `--sweep-count 5 --seed-step 101` when changing pool
|
||||||
|
selection, route terms, or formatter noise rules and you need more than one
|
||||||
|
seed of evidence.
|
||||||
|
|
||||||
## Editing Cheatsheet
|
## Editing Cheatsheet
|
||||||
|
|
||||||
@@ -991,8 +1185,9 @@ Before changing prompt behavior:
|
|||||||
3. Decide whether the bug is selection, raw wording, camera adaptation, or
|
3. Decide whether the bug is selection, raw wording, camera adaptation, or
|
||||||
formatter rewrite.
|
formatter rewrite.
|
||||||
4. Edit the smallest owning pool/template/function.
|
4. Edit the smallest owning pool/template/function.
|
||||||
5. Re-run a small simulation with fixed person/scene/category seeds and only the
|
5. Re-run `python tools/prompt_route_simulation.py --fail-on-issues` or a
|
||||||
target axis varied.
|
similarly small simulation with fixed person/scene/category seeds and only
|
||||||
|
the target axis varied.
|
||||||
|
|
||||||
The repo may have unrelated dirty files during interactive prompt work. Always
|
The repo may have unrelated dirty files during interactive prompt work. Always
|
||||||
stage only the intended files for commits.
|
stage only the intended files for commits.
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
DETAIL_LEVELS = ("balanced", "concise", "dense")
|
||||||
|
DEFAULT_DETAIL_LEVEL = "balanced"
|
||||||
|
|
||||||
|
|
||||||
|
def detail_level_choices() -> list[str]:
|
||||||
|
return list(DETAIL_LEVELS)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_detail_level(value: Any) -> str:
|
||||||
|
level = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
||||||
|
return level if level in DETAIL_LEVELS else DEFAULT_DETAIL_LEVEL
|
||||||
|
|
||||||
|
|
||||||
|
def detail_allows(level: Any, dense_only: bool = False) -> bool:
|
||||||
|
level = normalize_detail_level(level)
|
||||||
|
if dense_only:
|
||||||
|
return level == "dense"
|
||||||
|
return level != "concise"
|
||||||
+78
-2
@@ -4,6 +4,11 @@ import json
|
|||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import row_normalization as row_normalization_policy
|
||||||
|
except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.py`.
|
||||||
|
import row_normalization as row_normalization_policy
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_PROMPT_FIELD_LABELS = (
|
DEFAULT_PROMPT_FIELD_LABELS = (
|
||||||
"Ages",
|
"Ages",
|
||||||
@@ -11,6 +16,10 @@ DEFAULT_PROMPT_FIELD_LABELS = (
|
|||||||
"Cast",
|
"Cast",
|
||||||
"Cast descriptors",
|
"Cast descriptors",
|
||||||
"Characters",
|
"Characters",
|
||||||
|
"Softcore setup",
|
||||||
|
"Hardcore setup",
|
||||||
|
"POV participant",
|
||||||
|
"Body exposure",
|
||||||
"Scene",
|
"Scene",
|
||||||
"Setting",
|
"Setting",
|
||||||
"Pose",
|
"Pose",
|
||||||
@@ -19,7 +28,13 @@ DEFAULT_PROMPT_FIELD_LABELS = (
|
|||||||
"Facial expression",
|
"Facial expression",
|
||||||
"Facial expressions",
|
"Facial expressions",
|
||||||
"Clothing",
|
"Clothing",
|
||||||
|
"Clothing state",
|
||||||
|
"Visual clothing state",
|
||||||
|
"Outfit",
|
||||||
"Erotic outfit",
|
"Erotic outfit",
|
||||||
|
"Teaser outfit detail",
|
||||||
|
"Softcore visual reference",
|
||||||
|
"Visible remaining styling",
|
||||||
"Prop/detail",
|
"Prop/detail",
|
||||||
"Composition",
|
"Composition",
|
||||||
"Role graph",
|
"Role graph",
|
||||||
@@ -29,6 +44,26 @@ DEFAULT_PROMPT_FIELD_LABELS = (
|
|||||||
"Avoid",
|
"Avoid",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
INPUT_HINT_AUTO = "auto"
|
||||||
|
INPUT_HINT_METADATA = "metadata_json"
|
||||||
|
INPUT_HINT_PROMPT = "prompt"
|
||||||
|
INPUT_HINT_CAPTION_OR_PROMPT = "caption_or_prompt"
|
||||||
|
TEXT_INPUT_HINTS = (INPUT_HINT_PROMPT, INPUT_HINT_CAPTION_OR_PROMPT)
|
||||||
|
FORMATTER_INPUT_HINTS = (INPUT_HINT_AUTO, INPUT_HINT_METADATA, INPUT_HINT_PROMPT, INPUT_HINT_CAPTION_OR_PROMPT)
|
||||||
|
METADATA_INPUT_HINTS = (INPUT_HINT_AUTO, INPUT_HINT_METADATA)
|
||||||
|
|
||||||
|
_INPUT_HINT_ALIASES = {
|
||||||
|
"caption": INPUT_HINT_CAPTION_OR_PROMPT,
|
||||||
|
"caption_prompt": INPUT_HINT_CAPTION_OR_PROMPT,
|
||||||
|
"caption_or_text": INPUT_HINT_CAPTION_OR_PROMPT,
|
||||||
|
"metadata": INPUT_HINT_METADATA,
|
||||||
|
"metadata json": INPUT_HINT_METADATA,
|
||||||
|
"source_json": INPUT_HINT_AUTO,
|
||||||
|
"source text": INPUT_HINT_PROMPT,
|
||||||
|
"source_text": INPUT_HINT_PROMPT,
|
||||||
|
"text": INPUT_HINT_PROMPT,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def prompt_field_labels() -> tuple[str, ...]:
|
def prompt_field_labels() -> tuple[str, ...]:
|
||||||
return DEFAULT_PROMPT_FIELD_LABELS
|
return DEFAULT_PROMPT_FIELD_LABELS
|
||||||
@@ -53,18 +88,59 @@ def maybe_json(text: Any) -> dict[str, Any] | None:
|
|||||||
return value if isinstance(value, dict) else None
|
return value if isinstance(value, dict) else None
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_input_metadata(row: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
row = dict(row)
|
||||||
|
trigger = str(row.get("trigger") or "").strip()
|
||||||
|
if is_pair_metadata(row):
|
||||||
|
return row_normalization_policy.normalize_pair_metadata(row, active_trigger=trigger)
|
||||||
|
return row_normalization_policy.sanitize_metadata_row_text(row, active_trigger=trigger)
|
||||||
|
|
||||||
|
|
||||||
|
def is_pair_metadata(row: Any) -> bool:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
return False
|
||||||
|
soft_side = (
|
||||||
|
isinstance(row.get("softcore_row"), dict)
|
||||||
|
or bool(clean_text(row.get("softcore_prompt")))
|
||||||
|
or bool(clean_text(row.get("softcore_caption")))
|
||||||
|
)
|
||||||
|
hard_side = (
|
||||||
|
isinstance(row.get("hardcore_row"), dict)
|
||||||
|
or bool(clean_text(row.get("hardcore_prompt")))
|
||||||
|
or bool(clean_text(row.get("hardcore_caption")))
|
||||||
|
)
|
||||||
|
return soft_side and hard_side
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_input_hint(value: Any, *, text_hint: str = INPUT_HINT_PROMPT) -> str:
|
||||||
|
hint = clean_text(value).lower().replace("-", "_")
|
||||||
|
hint = _INPUT_HINT_ALIASES.get(hint, hint)
|
||||||
|
if hint in (INPUT_HINT_AUTO, INPUT_HINT_METADATA):
|
||||||
|
return hint
|
||||||
|
if hint in TEXT_INPUT_HINTS:
|
||||||
|
return text_hint if text_hint in TEXT_INPUT_HINTS else hint
|
||||||
|
return INPUT_HINT_AUTO
|
||||||
|
|
||||||
|
|
||||||
|
def input_hint_choices(*, text_hint: str = INPUT_HINT_PROMPT) -> list[str]:
|
||||||
|
text_hint = text_hint if text_hint in TEXT_INPUT_HINTS else INPUT_HINT_PROMPT
|
||||||
|
return [INPUT_HINT_AUTO, INPUT_HINT_METADATA, text_hint]
|
||||||
|
|
||||||
|
|
||||||
def row_from_inputs(
|
def row_from_inputs(
|
||||||
source_text: str,
|
source_text: str,
|
||||||
metadata_json: str,
|
metadata_json: str,
|
||||||
input_hint: str,
|
input_hint: str,
|
||||||
*,
|
*,
|
||||||
metadata_methods: tuple[str, ...] = ("auto", "metadata_json"),
|
metadata_methods: tuple[str, ...] = METADATA_INPUT_HINTS,
|
||||||
|
text_hint: str = INPUT_HINT_PROMPT,
|
||||||
) -> tuple[dict[str, Any] | None, str]:
|
) -> tuple[dict[str, Any] | None, str]:
|
||||||
|
input_hint = normalize_input_hint(input_hint, text_hint=text_hint)
|
||||||
if input_hint in metadata_methods:
|
if input_hint in metadata_methods:
|
||||||
for text, method in ((metadata_json, "metadata_json"), (source_text, "source_json")):
|
for text, method in ((metadata_json, "metadata_json"), (source_text, "source_json")):
|
||||||
row = maybe_json(text)
|
row = maybe_json(text)
|
||||||
if row is not None:
|
if row is not None:
|
||||||
return row, method
|
return normalize_input_metadata(row), method
|
||||||
return None, "text"
|
return None, "text"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import route_metadata as route_metadata_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import route_metadata as route_metadata_policy
|
||||||
|
|
||||||
|
|
||||||
|
PAIR_SIDES = ("softcore", "hardcore")
|
||||||
|
|
||||||
|
|
||||||
|
def route_trace_json(**values: Any) -> str:
|
||||||
|
trace: dict[str, Any] = {}
|
||||||
|
for key, value in values.items():
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
trace[key] = value
|
||||||
|
return json.dumps(trace, ensure_ascii=True, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _pair_selected_side(target: Any, selected_side: Any = "") -> str:
|
||||||
|
side = str(selected_side or "").strip().lower()
|
||||||
|
if side in PAIR_SIDES:
|
||||||
|
return side
|
||||||
|
target_side = str(target or "").strip().lower()
|
||||||
|
return target_side if target_side in PAIR_SIDES else "softcore"
|
||||||
|
|
||||||
|
|
||||||
|
def _add_if_value(trace: dict[str, Any], key: str, value: Any) -> None:
|
||||||
|
if value is None:
|
||||||
|
return
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
if isinstance(value, (list, tuple, set)) and not value:
|
||||||
|
return
|
||||||
|
trace[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def metadata_trace_fields(row: Any, *, target: Any = "", selected_side: Any = "") -> dict[str, Any]:
|
||||||
|
"""Return compact row metadata fields for formatter route traces.
|
||||||
|
|
||||||
|
The trace intentionally carries routing/debug identifiers, not full prompt
|
||||||
|
prose or cast descriptors.
|
||||||
|
"""
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
return {}
|
||||||
|
trace: dict[str, Any] = {}
|
||||||
|
source_row = row
|
||||||
|
if isinstance(row.get("softcore_row"), dict) or isinstance(row.get("hardcore_row"), dict):
|
||||||
|
side = _pair_selected_side(target, selected_side)
|
||||||
|
source_row = row.get(f"{side}_row") if isinstance(row.get(f"{side}_row"), dict) else {}
|
||||||
|
trace["metadata_kind"] = "pair"
|
||||||
|
trace["selected_side"] = side
|
||||||
|
else:
|
||||||
|
trace["metadata_kind"] = "row"
|
||||||
|
|
||||||
|
if not isinstance(source_row, dict):
|
||||||
|
return trace
|
||||||
|
|
||||||
|
_add_if_value(trace, "metadata_category", source_row.get("main_category") or source_row.get("category"))
|
||||||
|
_add_if_value(trace, "metadata_subcategory", source_row.get("subcategory"))
|
||||||
|
_add_if_value(trace, "action_family", route_metadata_policy.row_action_family(source_row))
|
||||||
|
_add_if_value(trace, "position_family", route_metadata_policy.row_position_family(source_row))
|
||||||
|
_add_if_value(trace, "position_key", source_row.get("position_key"))
|
||||||
|
_add_if_value(trace, "position_keys", route_metadata_policy.row_position_keys(source_row, include_unknown=True))
|
||||||
|
_add_if_value(trace, "scene_profile", source_row.get("scene_camera_profile_key"))
|
||||||
|
_add_if_value(trace, "pov_labels", source_row.get("pov_character_labels"))
|
||||||
|
return trace
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
FORMATTER_TARGETS = ("auto", "single", "softcore", "hardcore")
|
||||||
|
PAIR_SIDE_TARGETS = ("softcore", "hardcore")
|
||||||
|
DEFAULT_FORMATTER_TARGET = "auto"
|
||||||
|
DEFAULT_PAIR_SELECTED_SIDE = "softcore"
|
||||||
|
|
||||||
|
_TARGET_ALIASES = {
|
||||||
|
"soft": "softcore",
|
||||||
|
"soft_core": "softcore",
|
||||||
|
"hard": "hardcore",
|
||||||
|
"hard_core": "hardcore",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PairTargetPolicy:
|
||||||
|
target: str
|
||||||
|
pair_target: str
|
||||||
|
selected_side: str
|
||||||
|
include_softcore: bool
|
||||||
|
include_hardcore: bool
|
||||||
|
|
||||||
|
|
||||||
|
def target_choices() -> list[str]:
|
||||||
|
return list(FORMATTER_TARGETS)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_target(value: Any) -> str:
|
||||||
|
target = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
||||||
|
target = _TARGET_ALIASES.get(target, target)
|
||||||
|
return target if target in FORMATTER_TARGETS else DEFAULT_FORMATTER_TARGET
|
||||||
|
|
||||||
|
|
||||||
|
def pair_target(value: Any) -> str:
|
||||||
|
target = normalize_target(value)
|
||||||
|
return target if target in PAIR_SIDE_TARGETS else DEFAULT_FORMATTER_TARGET
|
||||||
|
|
||||||
|
|
||||||
|
def pair_selected_side(value: Any, default: str = DEFAULT_PAIR_SELECTED_SIDE) -> str:
|
||||||
|
side = pair_target(value)
|
||||||
|
if side in PAIR_SIDE_TARGETS:
|
||||||
|
return side
|
||||||
|
return default if default in PAIR_SIDE_TARGETS else DEFAULT_PAIR_SELECTED_SIDE
|
||||||
|
|
||||||
|
|
||||||
|
def pair_policy(value: Any, *, selected_default: str = DEFAULT_PAIR_SELECTED_SIDE) -> PairTargetPolicy:
|
||||||
|
target = normalize_target(value)
|
||||||
|
side_target = pair_target(target)
|
||||||
|
selected_side = pair_selected_side(side_target, selected_default)
|
||||||
|
return PairTargetPolicy(
|
||||||
|
target=target,
|
||||||
|
pair_target=side_target,
|
||||||
|
selected_side=selected_side,
|
||||||
|
include_softcore=side_target in ("auto", "softcore"),
|
||||||
|
include_hardcore=side_target in ("auto", "hardcore"),
|
||||||
|
)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -25,28 +26,72 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
|||||||
|
|
||||||
|
|
||||||
ACTION_CLIMAX = "climax"
|
ACTION_CLIMAX = "climax"
|
||||||
|
ACTION_ANAL = "anal"
|
||||||
ACTION_FOREPLAY = "foreplay"
|
ACTION_FOREPLAY = "foreplay"
|
||||||
|
ACTION_MANUAL = "manual"
|
||||||
ACTION_OUTERCOURSE = "outercourse"
|
ACTION_OUTERCOURSE = "outercourse"
|
||||||
ACTION_ORAL = "oral"
|
ACTION_ORAL = "oral"
|
||||||
ACTION_PENETRATION = "penetration"
|
ACTION_PENETRATION = "penetration"
|
||||||
|
ACTION_THREESOME = "threesome"
|
||||||
|
ACTION_GROUP = "group"
|
||||||
ACTION_TOY_DOUBLE = "toy_double"
|
ACTION_TOY_DOUBLE = "toy_double"
|
||||||
ACTION_DEFAULT = "default"
|
ACTION_DEFAULT = "default"
|
||||||
|
|
||||||
HARDCORE_ACTION_FAMILY_CHOICES = {
|
HARDCORE_ACTION_FAMILY_CHOICES = {
|
||||||
ACTION_CLIMAX,
|
ACTION_CLIMAX,
|
||||||
|
ACTION_ANAL,
|
||||||
ACTION_FOREPLAY,
|
ACTION_FOREPLAY,
|
||||||
|
ACTION_MANUAL,
|
||||||
ACTION_OUTERCOURSE,
|
ACTION_OUTERCOURSE,
|
||||||
ACTION_ORAL,
|
ACTION_ORAL,
|
||||||
ACTION_PENETRATION,
|
ACTION_PENETRATION,
|
||||||
|
ACTION_THREESOME,
|
||||||
|
ACTION_GROUP,
|
||||||
ACTION_TOY_DOUBLE,
|
ACTION_TOY_DOUBLE,
|
||||||
ACTION_DEFAULT,
|
ACTION_DEFAULT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def normalize_hardcore_action_family(value: Any, default: str = "") -> str:
|
def normalize_hardcore_action_family(value: Any, default: str = "") -> str:
|
||||||
text = str(value or "").strip().lower()
|
text = re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().lower()).strip("_")
|
||||||
if text == "penetrative":
|
aliases = {
|
||||||
text = ACTION_PENETRATION
|
"penetrative": ACTION_PENETRATION,
|
||||||
|
"penetrative_sex": ACTION_PENETRATION,
|
||||||
|
"penetration_sex": ACTION_PENETRATION,
|
||||||
|
"vaginal": ACTION_PENETRATION,
|
||||||
|
"vaginal_penetration": ACTION_PENETRATION,
|
||||||
|
"double": ACTION_TOY_DOUBLE,
|
||||||
|
"double_penetration": ACTION_TOY_DOUBLE,
|
||||||
|
"toy_double_penetration": ACTION_TOY_DOUBLE,
|
||||||
|
"toy_assisted_double": ACTION_TOY_DOUBLE,
|
||||||
|
"toy_assisted_double_penetration": ACTION_TOY_DOUBLE,
|
||||||
|
"anal": ACTION_ANAL,
|
||||||
|
"anal_sex": ACTION_ANAL,
|
||||||
|
"anal_penetration": ACTION_ANAL,
|
||||||
|
"outer_course": ACTION_OUTERCOURSE,
|
||||||
|
"outercourse_sex": ACTION_OUTERCOURSE,
|
||||||
|
"three_person": ACTION_THREESOME,
|
||||||
|
"three_person_action": ACTION_THREESOME,
|
||||||
|
"threesome": ACTION_THREESOME,
|
||||||
|
"threesomes": ACTION_THREESOME,
|
||||||
|
"threeway": ACTION_THREESOME,
|
||||||
|
"three_way": ACTION_THREESOME,
|
||||||
|
"group": ACTION_GROUP,
|
||||||
|
"group_sex": ACTION_GROUP,
|
||||||
|
"group_sex_orgy": ACTION_GROUP,
|
||||||
|
"orgy": ACTION_GROUP,
|
||||||
|
"manual": ACTION_MANUAL,
|
||||||
|
"manual_stimulation": ACTION_MANUAL,
|
||||||
|
"interaction": ACTION_FOREPLAY,
|
||||||
|
"body_worship": ACTION_FOREPLAY,
|
||||||
|
"body_worship_touching": ACTION_FOREPLAY,
|
||||||
|
"foreplay_teasing": ACTION_FOREPLAY,
|
||||||
|
"cumshot": ACTION_CLIMAX,
|
||||||
|
"cumshot_climax": ACTION_CLIMAX,
|
||||||
|
"orgasm_aftermath": ACTION_CLIMAX,
|
||||||
|
"oral_sex": ACTION_ORAL,
|
||||||
|
}
|
||||||
|
text = aliases.get(text, text)
|
||||||
return text if text in HARDCORE_ACTION_FAMILY_CHOICES else default
|
return text if text in HARDCORE_ACTION_FAMILY_CHOICES else default
|
||||||
|
|
||||||
|
|
||||||
@@ -86,16 +131,34 @@ def source_hardcore_action_family(
|
|||||||
inferred = infer_hardcore_action_family(role_graph, hard_item, composition, axis_values)
|
inferred = infer_hardcore_action_family(role_graph, hard_item, composition, axis_values)
|
||||||
if inferred in (ACTION_CLIMAX, ACTION_TOY_DOUBLE):
|
if inferred in (ACTION_CLIMAX, ACTION_TOY_DOUBLE):
|
||||||
return inferred
|
return inferred
|
||||||
family = str(source_family or "").strip().lower()
|
family = re.sub(r"[^a-z0-9]+", "_", str(source_family or "").strip().lower()).strip("_")
|
||||||
|
family = {
|
||||||
|
"penetration": "penetrative",
|
||||||
|
"penetrative_sex": "penetrative",
|
||||||
|
"outer_course": "outercourse",
|
||||||
|
"outercourse_sex": "outercourse",
|
||||||
|
"manual_stimulation": "manual",
|
||||||
|
"foreplay_teasing": "foreplay",
|
||||||
|
"body_worship": "interaction",
|
||||||
|
"body_worship_touching": "interaction",
|
||||||
|
"clothing_position_transitions": "interaction",
|
||||||
|
"dominant_guidance": "interaction",
|
||||||
|
"camera_performance": "interaction",
|
||||||
|
"group_coordination": "interaction",
|
||||||
|
"cumshot": "climax",
|
||||||
|
"cumshot_climax": "climax",
|
||||||
|
"oral_sex": "oral",
|
||||||
|
}.get(family, family)
|
||||||
source_mapping = {
|
source_mapping = {
|
||||||
"penetrative": ACTION_PENETRATION,
|
"penetrative": ACTION_PENETRATION,
|
||||||
|
"anal": ACTION_ANAL,
|
||||||
"foreplay": ACTION_FOREPLAY,
|
"foreplay": ACTION_FOREPLAY,
|
||||||
"interaction": ACTION_FOREPLAY,
|
"interaction": ACTION_FOREPLAY,
|
||||||
"manual": ACTION_FOREPLAY,
|
"manual": ACTION_MANUAL,
|
||||||
"oral": ACTION_ORAL,
|
"oral": ACTION_ORAL,
|
||||||
"outercourse": ACTION_OUTERCOURSE,
|
"outercourse": ACTION_OUTERCOURSE,
|
||||||
|
"threesome": ACTION_THREESOME,
|
||||||
|
"group": ACTION_GROUP,
|
||||||
"climax": ACTION_CLIMAX,
|
"climax": ACTION_CLIMAX,
|
||||||
}
|
}
|
||||||
if family == "anal":
|
|
||||||
return ACTION_DEFAULT
|
|
||||||
return source_mapping.get(family, inferred)
|
return source_mapping.get(family, inferred)
|
||||||
|
|||||||
+113
-16
@@ -5,6 +5,11 @@ import re
|
|||||||
from string import Formatter
|
from string import Formatter
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
HARDCORE_POSITION_FAMILY_CHOICES = [
|
HARDCORE_POSITION_FAMILY_CHOICES = [
|
||||||
"any",
|
"any",
|
||||||
@@ -240,6 +245,29 @@ def _entry_text(item: Any) -> str:
|
|||||||
return str(item).strip()
|
return str(item).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_tokens(item: Any, keys: tuple[str, ...]) -> set[str]:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
return set()
|
||||||
|
tokens: set[str] = set()
|
||||||
|
for key in keys:
|
||||||
|
for value in _list_from(item.get(key)):
|
||||||
|
token = re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().lower()).strip("_")
|
||||||
|
if token and token != "any":
|
||||||
|
tokens.add(token)
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
def _entry_position_keys(item: Any) -> list[str]:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
return []
|
||||||
|
values: list[Any] = []
|
||||||
|
if item.get("position_keys") is not None:
|
||||||
|
values.extend(_list_from(item.get("position_keys")))
|
||||||
|
if item.get("position_key") is not None:
|
||||||
|
values.append(item.get("position_key"))
|
||||||
|
return normalize_hardcore_position_values(values)
|
||||||
|
|
||||||
|
|
||||||
def hardcore_position_family_choices() -> list[str]:
|
def hardcore_position_family_choices() -> list[str]:
|
||||||
return list(HARDCORE_POSITION_FAMILY_CHOICES)
|
return list(HARDCORE_POSITION_FAMILY_CHOICES)
|
||||||
|
|
||||||
@@ -253,7 +281,36 @@ def hardcore_position_key_choices() -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def normalize_hardcore_position_family(value: Any, default: str = "any") -> str:
|
def normalize_hardcore_position_family(value: Any, default: str = "any") -> str:
|
||||||
text = str(value or default).strip()
|
text = re.sub(r"[^a-z0-9]+", "_", str(value or default).strip().lower()).strip("_")
|
||||||
|
aliases = {
|
||||||
|
"penetration": "penetrative",
|
||||||
|
"penetrative_sex": "penetrative",
|
||||||
|
"penetration_sex": "penetrative",
|
||||||
|
"vaginal": "penetrative",
|
||||||
|
"vaginal_penetration": "penetrative",
|
||||||
|
"foreplay_teasing": "foreplay",
|
||||||
|
"body_worship": "interaction",
|
||||||
|
"body_worship_touching": "interaction",
|
||||||
|
"clothing_position_transitions": "interaction",
|
||||||
|
"dominant_guidance": "interaction",
|
||||||
|
"camera_performance": "interaction",
|
||||||
|
"group_coordination": "interaction",
|
||||||
|
"aftercare_cleanup": "interaction",
|
||||||
|
"manual_stimulation": "manual",
|
||||||
|
"oral_sex": "oral",
|
||||||
|
"outer_course": "outercourse",
|
||||||
|
"outercourse_sex": "outercourse",
|
||||||
|
"anal_double_penetration": "anal",
|
||||||
|
"three_some": "threesome",
|
||||||
|
"threesomes": "threesome",
|
||||||
|
"group_sex": "group",
|
||||||
|
"group_sex_orgy": "group",
|
||||||
|
"orgy": "group",
|
||||||
|
"cumshot": "climax",
|
||||||
|
"cumshot_climax": "climax",
|
||||||
|
"orgasm_aftermath": "climax",
|
||||||
|
}
|
||||||
|
text = aliases.get(text, text)
|
||||||
return text if text in HARDCORE_POSITION_FAMILY_CHOICES else default
|
return text if text in HARDCORE_POSITION_FAMILY_CHOICES else default
|
||||||
|
|
||||||
|
|
||||||
@@ -468,7 +525,8 @@ def hardcore_position_template_required(config: dict[str, Any]) -> bool:
|
|||||||
|
|
||||||
def hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
|
def hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
|
||||||
family = normalize_hardcore_position_family(config.get("family"))
|
family = normalize_hardcore_position_family(config.get("family"))
|
||||||
allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"]))
|
base_allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"]))
|
||||||
|
allowed = set(base_allowed)
|
||||||
if not config.get("allow_penetration", True):
|
if not config.get("allow_penetration", True):
|
||||||
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
|
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
|
||||||
if not config.get("allow_foreplay", True):
|
if not config.get("allow_foreplay", True):
|
||||||
@@ -497,7 +555,11 @@ def hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
|
|||||||
allowed.discard("cumshot_climax")
|
allowed.discard("cumshot_climax")
|
||||||
if not config.get("allow_double", True) and family == "anal":
|
if not config.get("allow_double", True) and family == "anal":
|
||||||
allowed.add("anal_double_penetration")
|
allowed.add("anal_double_penetration")
|
||||||
return allowed or set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])
|
if allowed:
|
||||||
|
return allowed
|
||||||
|
if family != "any":
|
||||||
|
return base_allowed
|
||||||
|
return set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])
|
||||||
|
|
||||||
|
|
||||||
def is_hardcore_sexual_category(category: dict[str, Any]) -> bool:
|
def is_hardcore_sexual_category(category: dict[str, Any]) -> bool:
|
||||||
@@ -633,10 +695,42 @@ def hardcore_text_blocked_by_action(text: str, axis_name: str, config: dict[str,
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def hardcore_entry_blocked_by_action(entry: Any, axis_name: str, config: dict[str, Any]) -> bool:
|
||||||
|
action_tokens = _metadata_tokens(entry, ("action_family", "action_type"))
|
||||||
|
family_tokens = _metadata_tokens(entry, ("position_family", "family"))
|
||||||
|
position_keys = set(_entry_position_keys(entry))
|
||||||
|
route_tokens = action_tokens | family_tokens
|
||||||
|
|
||||||
|
if not config.get("allow_toys", True) and action_tokens & {"toy", "toy_double"}:
|
||||||
|
return True
|
||||||
|
if not config.get("allow_double", True) and (action_tokens & {"double", "toy_double"} or "front_back" in position_keys):
|
||||||
|
return True
|
||||||
|
if not config.get("allow_anal", True) and "anal" in route_tokens:
|
||||||
|
return True
|
||||||
|
if not config.get("allow_oral", True) and "oral" in route_tokens:
|
||||||
|
return True
|
||||||
|
if not config.get("allow_outercourse", True) and "outercourse" in route_tokens:
|
||||||
|
return True
|
||||||
|
if not config.get("allow_penetration", True) and route_tokens & {"penetration", "penetrative", "toy_double", "anal"}:
|
||||||
|
return True
|
||||||
|
if not config.get("allow_foreplay", True) and "foreplay" in route_tokens:
|
||||||
|
return True
|
||||||
|
if not config.get("allow_interaction", True) and "interaction" in route_tokens:
|
||||||
|
return True
|
||||||
|
if not config.get("allow_manual", True) and "manual" in route_tokens:
|
||||||
|
return True
|
||||||
|
if not config.get("allow_climax", True) and "climax" in route_tokens:
|
||||||
|
return True
|
||||||
|
return hardcore_text_blocked_by_action(_entry_text(entry), axis_name, config)
|
||||||
|
|
||||||
|
|
||||||
def hardcore_position_entry_matches(entry: Any, config: dict[str, Any]) -> bool:
|
def hardcore_position_entry_matches(entry: Any, config: dict[str, Any]) -> bool:
|
||||||
positions = config.get("positions") or []
|
positions = config.get("positions") or []
|
||||||
if not positions:
|
if not positions:
|
||||||
return True
|
return True
|
||||||
|
metadata_keys = _entry_position_keys(entry)
|
||||||
|
if metadata_keys:
|
||||||
|
return bool(set(metadata_keys) & set(positions))
|
||||||
text = _entry_text(entry).lower()
|
text = _entry_text(entry).lower()
|
||||||
for position in positions:
|
for position in positions:
|
||||||
if any(term in text for term in HARDCORE_POSITION_KEY_MATCHES.get(position, ())):
|
if any(term in text for term in HARDCORE_POSITION_KEY_MATCHES.get(position, ())):
|
||||||
@@ -648,12 +742,16 @@ def hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> boo
|
|||||||
selected = set(config.get("positions") or [])
|
selected = set(config.get("positions") or [])
|
||||||
if not selected:
|
if not selected:
|
||||||
return False
|
return False
|
||||||
text = _entry_text(entry).lower()
|
metadata_keys = _entry_position_keys(entry)
|
||||||
matched = {
|
if metadata_keys:
|
||||||
position
|
matched = set(metadata_keys)
|
||||||
for position, terms in HARDCORE_POSITION_KEY_MATCHES.items()
|
else:
|
||||||
if any(term in text for term in terms)
|
text = _entry_text(entry).lower()
|
||||||
}
|
matched = {
|
||||||
|
position
|
||||||
|
for position, terms in HARDCORE_POSITION_KEY_MATCHES.items()
|
||||||
|
if any(term in text for term in terms)
|
||||||
|
}
|
||||||
return bool(matched) and not bool(matched & selected)
|
return bool(matched) and not bool(matched & selected)
|
||||||
|
|
||||||
|
|
||||||
@@ -678,7 +776,7 @@ def filter_hardcore_axis(axis_name: str, values: list[Any], config: dict[str, An
|
|||||||
filtered = [
|
filtered = [
|
||||||
value
|
value
|
||||||
for value in values
|
for value in values
|
||||||
if not hardcore_text_blocked_by_action(_entry_text(value), axis_name, config)
|
if not hardcore_entry_blocked_by_action(value, axis_name, config)
|
||||||
and not (axis_name not in HARDCORE_POSITION_AXIS_KEYS and hardcore_position_entry_conflicts(value, config))
|
and not (axis_name not in HARDCORE_POSITION_AXIS_KEYS and hardcore_position_entry_conflicts(value, config))
|
||||||
and (axis_name not in HARDCORE_POSITION_AXIS_KEYS or hardcore_position_entry_matches(value, config))
|
and (axis_name not in HARDCORE_POSITION_AXIS_KEYS or hardcore_position_entry_matches(value, config))
|
||||||
]
|
]
|
||||||
@@ -692,8 +790,10 @@ def filter_hardcore_templates(templates: list[Any], config: dict[str, Any]) -> l
|
|||||||
for template in templates:
|
for template in templates:
|
||||||
text = _entry_text(template)
|
text = _entry_text(template)
|
||||||
fields = {key for _, key, _, _ in Formatter().parse(text) if key}
|
fields = {key for _, key, _, _ in Formatter().parse(text) if key}
|
||||||
blocked = hardcore_position_template_required(config) and not bool(fields & HARDCORE_POSITION_AXIS_KEYS)
|
has_position_route = bool(fields & HARDCORE_POSITION_AXIS_KEYS) or bool(_entry_position_keys(template))
|
||||||
blocked = blocked or any(hardcore_text_blocked_by_action(text, field, config) for field in fields | {""})
|
blocked = hardcore_position_template_required(config) and not has_position_route
|
||||||
|
blocked = blocked or hardcore_entry_blocked_by_action(template, "", config)
|
||||||
|
blocked = blocked or any(hardcore_text_blocked_by_action(text, field, config) for field in fields)
|
||||||
if not blocked:
|
if not blocked:
|
||||||
filtered.append(template)
|
filtered.append(template)
|
||||||
return filtered or templates
|
return filtered or templates
|
||||||
@@ -757,10 +857,7 @@ def hardcore_source_position_family(subcategory: dict[str, Any], config: dict[st
|
|||||||
|
|
||||||
|
|
||||||
def hardcore_position_keys(*parts: Any, axis_values: dict[str, Any] | None = None) -> list[str]:
|
def hardcore_position_keys(*parts: Any, axis_values: dict[str, Any] | None = None) -> list[str]:
|
||||||
text_parts = [str(part or "") for part in parts if str(part or "").strip()]
|
text = item_axis_policy.context_text(*parts, axis_values=axis_values)
|
||||||
if isinstance(axis_values, dict):
|
|
||||||
text_parts.extend(str(value or "") for value in axis_values.values() if str(value or "").strip())
|
|
||||||
text = " ".join(text_parts).lower()
|
|
||||||
if not text:
|
if not text:
|
||||||
return []
|
return []
|
||||||
keys: list[str] = []
|
keys: list[str] = []
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||||
return " ".join(
|
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||||
str(part or "").lower()
|
|
||||||
for part in (
|
|
||||||
item_text,
|
|
||||||
*((item_axis_values or {}).values()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _anal_position_graph(woman: str, man: str, context: str) -> str:
|
def _anal_position_graph(woman: str, man: str, context: str) -> str:
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||||
return " ".join(
|
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||||
str(part or "").lower()
|
|
||||||
for part in (
|
|
||||||
item_text,
|
|
||||||
*((item_axis_values or {}).values()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _mentions_ass(text: str) -> bool:
|
def _mentions_ass(text: str) -> bool:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import random
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
from .hardcore_role_anal import build_anal_or_double_role_graph
|
from .hardcore_role_anal import build_anal_or_double_role_graph
|
||||||
from .hardcore_role_climax import build_climax_role_graph
|
from .hardcore_role_climax import build_climax_role_graph
|
||||||
from .hardcore_role_fallback import (
|
from .hardcore_role_fallback import (
|
||||||
@@ -23,6 +24,7 @@ try:
|
|||||||
from .hardcore_role_outercourse import build_outercourse_role_graph
|
from .hardcore_role_outercourse import build_outercourse_role_graph
|
||||||
from .hardcore_role_penetration import build_penetration_role_graph
|
from .hardcore_role_penetration import build_penetration_role_graph
|
||||||
except ImportError: # Allows local smoke tests with `python -c`.
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
|
import item_axis_policy
|
||||||
from hardcore_role_anal import build_anal_or_double_role_graph
|
from hardcore_role_anal import build_anal_or_double_role_graph
|
||||||
from hardcore_role_climax import build_climax_role_graph
|
from hardcore_role_climax import build_climax_role_graph
|
||||||
from hardcore_role_fallback import (
|
from hardcore_role_fallback import (
|
||||||
@@ -85,7 +87,7 @@ def build_hardcore_role_graph(
|
|||||||
men = participants["men"]
|
men = participants["men"]
|
||||||
people = participants["people"]
|
people = participants["people"]
|
||||||
slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower()
|
slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower()
|
||||||
item_text = " ".join((item_axis_values or {}).values()).lower()
|
item_text = item_axis_policy.context_text(axis_values=item_axis_values)
|
||||||
|
|
||||||
def any_person(exclude: set[str] | None = None) -> str:
|
def any_person(exclude: set[str] | None = None) -> str:
|
||||||
exclude = exclude or set()
|
exclude = exclude or set()
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||||
return " ".join(
|
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||||
str(part or "").lower()
|
|
||||||
for part in (
|
|
||||||
item_text,
|
|
||||||
*((item_axis_values or {}).values()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_foreplay_role_graph(
|
def build_foreplay_role_graph(
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||||
return " ".join(
|
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||||
str(part or "").lower()
|
|
||||||
for part in (
|
|
||||||
item_text,
|
|
||||||
*((item_axis_values or {}).values()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _oral_direction(text: str) -> tuple[bool, bool]:
|
def _oral_direction(text: str) -> tuple[bool, bool]:
|
||||||
@@ -54,7 +53,7 @@ def build_oral_role_graph(
|
|||||||
item_axis_values: dict[str, Any] | None = None,
|
item_axis_values: dict[str, Any] | None = None,
|
||||||
pov_labels: list[str] | None = None,
|
pov_labels: list[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
position_text = str((item_axis_values or {}).get("position") or "").lower()
|
position_text = item_axis_policy.key_text(item_axis_values, "position")
|
||||||
text = _context_text(item_text, item_axis_values)
|
text = _context_text(item_text, item_axis_values)
|
||||||
man_is_pov = man in set(pov_labels or [])
|
man_is_pov = man in set(pov_labels or [])
|
||||||
woman_gives, man_gives = _oral_direction(text)
|
woman_gives, man_gives = _oral_direction(text)
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
from . import outercourse_action_policy as outercourse_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
import outercourse_action_policy as outercourse_policy
|
||||||
|
|
||||||
|
|
||||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||||
return " ".join(
|
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||||
str(part or "").lower()
|
|
||||||
for part in (
|
|
||||||
item_text,
|
|
||||||
*((item_axis_values or {}).values()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_outercourse_role_graph(
|
def build_outercourse_role_graph(
|
||||||
@@ -20,40 +21,48 @@ def build_outercourse_role_graph(
|
|||||||
item_axis_values: dict[str, Any] | None = None,
|
item_axis_values: dict[str, Any] | None = None,
|
||||||
pov_labels: list[str] | None = None,
|
pov_labels: list[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
position_text = str((item_axis_values or {}).get("position") or "").lower()
|
position_text = item_axis_policy.key_text(item_axis_values, "position")
|
||||||
text = _context_text(item_text, item_axis_values)
|
text = _context_text(item_text, item_axis_values)
|
||||||
|
action_kind = outercourse_policy.infer_outercourse_action_kind(position_text)
|
||||||
|
if action_kind == outercourse_policy.OUTERCOURSE_GENERIC:
|
||||||
|
action_kind = outercourse_policy.infer_outercourse_action_kind(text)
|
||||||
man_is_pov = man in set(pov_labels or [])
|
man_is_pov = man in set(pov_labels or [])
|
||||||
if any(term in text for term in ("boobjob", "titjob", "breast-sex", "breast sex")):
|
if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
|
||||||
if man_is_pov:
|
if man_is_pov:
|
||||||
return (
|
return (
|
||||||
f"{woman} kneels between the POV viewer's open thighs with her torso bent forward over his pelvis and shoulders low, "
|
f"{woman} kneels low between the POV viewer's open thighs with her torso bent forward over his pelvis, "
|
||||||
"both hands lifting and pressing her breasts tightly around the POV viewer's penis shaft while the glans sits just below her lips."
|
"both hands pushing her breasts inward around the POV viewer's penis, the penis held between her breasts in the lower foreground, "
|
||||||
|
"her chin and lips directly above the glans at the tip."
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
f"{woman} kneels between {man}'s open thighs with her torso bent forward over his pelvis and shoulders low while {man} sits with legs apart, "
|
f"{man} sits with legs apart while {woman} kneels low between his open thighs with her torso bent forward over his pelvis, "
|
||||||
f"{woman}'s hands lifting and pressing her breasts tightly around {man}'s penis shaft while the glans sits just below her lips."
|
f"{woman}'s hands pushing her breasts inward around {man}'s penis, the penis held between her breasts, "
|
||||||
|
"her chin and lips directly above the glans at the tip."
|
||||||
)
|
)
|
||||||
if any(term in text for term in ("testicle", "balls-licking", "balls licking", "balls and mouth", "balls held")):
|
if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
|
||||||
if man_is_pov:
|
if man_is_pov:
|
||||||
return (
|
return (
|
||||||
f"{woman} kneels very low between the POV viewer's open thighs with her torso bent forward and shoulders between his knees, "
|
f"{woman} bends forward and kneels very low between the POV viewer's open thighs with her shoulders between his knees, "
|
||||||
"head tucked under the penis shaft at the base of the penis, mouth and tongue on the POV viewer's balls while his penis points upward above her face."
|
"her face below the POV viewer's penis at testicle height, mouth and tongue on the POV viewer's balls, "
|
||||||
|
"while his penis points upward in the lower foreground above her forehead."
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
f"{man} sits with legs apart while {woman} kneels very low between his open thighs with her torso bent forward and shoulders between his knees, "
|
f"{man} sits with legs apart while {woman} kneels very low between his open thighs with her torso bent forward and shoulders between his knees, "
|
||||||
f"head tucked under the penis shaft at the base of his penis, mouth and tongue on his balls while {man}'s penis points upward above her face."
|
f"{woman}'s face below {man}'s penis at testicle height, mouth and tongue on his balls, while {man}'s penis points upward above her forehead."
|
||||||
)
|
)
|
||||||
if "penis-licking" in position_text or "penis licking" in text or "tongue along" in text or "tongue licking" in text:
|
if action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING:
|
||||||
if man_is_pov:
|
if man_is_pov:
|
||||||
return (
|
return (
|
||||||
f"{woman} bends forward between the POV viewer's open thighs, head low under the POV viewer's penis with her face directly under the penis, "
|
f"{woman} bends forward between the POV viewer's open thighs with her head low under the POV viewer's penis, "
|
||||||
"tongue running along the underside from the penis shaft to the glans while one hand steadies the base of the penis."
|
"her face just under the penis while her tongue touches the underside from the base toward the glans at the tip, "
|
||||||
|
"one hand steadying the base of the POV viewer's penis."
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
f"{woman} bends forward between {man}'s open thighs, head low under {man}'s penis with her face directly under the penis, "
|
f"{woman} bends forward between {man}'s open thighs with her head low under {man}'s penis, "
|
||||||
f"tongue running along the underside from the penis shaft to the glans while one hand steadies the base of the penis."
|
f"her face just under the penis while her tongue touches the underside from the base toward the glans at the tip, "
|
||||||
|
f"one hand steadying the base of {man}'s penis."
|
||||||
)
|
)
|
||||||
if "footjob" in text or "soles" in text or "toes curled" in text or "feet stroking" in text:
|
if action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
|
||||||
if man_is_pov:
|
if man_is_pov:
|
||||||
return (
|
return (
|
||||||
f"{woman} faces the POV viewer with her hips back, torso visible behind her raised legs, and both knees bent open toward the camera, "
|
f"{woman} faces the POV viewer with her hips back, torso visible behind her raised legs, and both knees bent open toward the camera, "
|
||||||
@@ -63,15 +72,17 @@ def build_outercourse_role_graph(
|
|||||||
f"{man} reclines with hips forward while {woman} faces him with her hips back and both knees bent open, "
|
f"{man} reclines with hips forward while {woman} faces him with her hips back and both knees bent open, "
|
||||||
f"wrapping both soles around {man}'s penis shaft while the contact stays centered."
|
f"wrapping both soles around {man}'s penis shaft while the contact stays centered."
|
||||||
)
|
)
|
||||||
if "handjob" in position_text or "handjob" in text or "hand job" in text or "hand wrapped" in text:
|
if action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
|
||||||
if man_is_pov:
|
if man_is_pov:
|
||||||
return (
|
return (
|
||||||
f"{woman} kneels between the POV viewer's open thighs with her torso leaning forward and face visible behind the penis shaft, "
|
f"{woman} kneels between the POV viewer's open thighs with her torso leaning forward and face visible behind the POV viewer's penis, "
|
||||||
"one hand wrapped around the POV viewer's penis shaft while the other hand steadies the base of the penis as she strokes toward the glans."
|
"one hand grips and strokes the POV viewer's penis in the lower foreground while the other hand steadies its base, "
|
||||||
|
"thumb and fingers visible around the penis as she strokes toward the glans."
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
f"{woman} kneels between {man}'s open thighs with her torso leaning forward and face visible behind the penis shaft, "
|
f"{woman} kneels between {man}'s open thighs with her torso leaning forward and face visible behind {man}'s penis, "
|
||||||
f"one hand wrapped around {man}'s penis shaft while the other hand steadies the base of the penis as she strokes toward the glans."
|
f"one hand grips and strokes {man}'s penis while the other hand steadies its base, "
|
||||||
|
"thumb and fingers visible around the penis as she strokes toward the glans."
|
||||||
)
|
)
|
||||||
if man_is_pov:
|
if man_is_pov:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||||
return " ".join(
|
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||||
str(part or "").lower()
|
|
||||||
for part in (
|
|
||||||
item_text,
|
|
||||||
*((item_axis_values or {}).values()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_penetration_role_graph(
|
def build_penetration_role_graph(
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
PLACEHOLDER_VALUES = {"", "any", "auto", "random", "none", "null"}
|
||||||
|
PREFERRED_VALUE_KEYS = ("text", "prompt", "template", "value", "name")
|
||||||
|
METADATA_AXIS_KEYS = {"action_family", "position_family", "position_key", "position_keys"}
|
||||||
|
ACTION_CONTEXT_PRIORITY = (
|
||||||
|
"position",
|
||||||
|
"body_position",
|
||||||
|
"body_arrangement",
|
||||||
|
"arrangement",
|
||||||
|
"angle",
|
||||||
|
"surface",
|
||||||
|
"body_contact",
|
||||||
|
"leg_detail",
|
||||||
|
"outer_act",
|
||||||
|
"contact_detail",
|
||||||
|
"texture_detail",
|
||||||
|
"hand_detail",
|
||||||
|
"visibility",
|
||||||
|
"expression_detail",
|
||||||
|
"oral_act",
|
||||||
|
"oral_detail",
|
||||||
|
"penetration_act",
|
||||||
|
"penetration_detail",
|
||||||
|
"anal_act",
|
||||||
|
"double_act",
|
||||||
|
"threesome_act",
|
||||||
|
"group_act",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_text(value: Any) -> str:
|
||||||
|
text = "" if value is None else str(value)
|
||||||
|
text = text.replace("\n", " ")
|
||||||
|
text = re.sub(r"\s+", " ", text).strip()
|
||||||
|
text = re.sub(r"\s+([,.;:])", r"\1", text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def value_texts(value: Any) -> list[str]:
|
||||||
|
if isinstance(value, str):
|
||||||
|
text = clean_text(value).strip(" .")
|
||||||
|
return [text] if text and text.lower() not in PLACEHOLDER_VALUES else []
|
||||||
|
if isinstance(value, (int, float, bool)) or value is None:
|
||||||
|
return []
|
||||||
|
if isinstance(value, list):
|
||||||
|
texts: list[str] = []
|
||||||
|
for item in value:
|
||||||
|
texts.extend(value_texts(item))
|
||||||
|
return texts
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for preferred in PREFERRED_VALUE_KEYS:
|
||||||
|
preferred_texts = value_texts(value.get(preferred))
|
||||||
|
if preferred_texts:
|
||||||
|
return preferred_texts
|
||||||
|
texts: list[str] = []
|
||||||
|
for item in value.values():
|
||||||
|
texts.extend(value_texts(item))
|
||||||
|
return texts
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def axis_value_texts(
|
||||||
|
axis_values: Any,
|
||||||
|
*,
|
||||||
|
priority: tuple[str, ...] = (),
|
||||||
|
include_unprioritized: bool = True,
|
||||||
|
skip_keys: set[str] | frozenset[str] | tuple[str, ...] = (),
|
||||||
|
existing_text: Any = "",
|
||||||
|
) -> list[str]:
|
||||||
|
if not isinstance(axis_values, dict):
|
||||||
|
return []
|
||||||
|
skipped = {str(key) for key in skip_keys}
|
||||||
|
keys: list[str] = []
|
||||||
|
for key in priority:
|
||||||
|
if key in axis_values and key not in skipped and key not in keys:
|
||||||
|
keys.append(key)
|
||||||
|
if include_unprioritized:
|
||||||
|
for key in axis_values:
|
||||||
|
if key not in skipped and key not in keys:
|
||||||
|
keys.append(key)
|
||||||
|
|
||||||
|
existing = clean_text(existing_text).lower()
|
||||||
|
texts: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for key in keys:
|
||||||
|
for text in value_texts(axis_values.get(key)):
|
||||||
|
normalized = clean_text(text).strip(" .")
|
||||||
|
lower = normalized.lower()
|
||||||
|
if not normalized or lower in seen or (existing and lower in existing):
|
||||||
|
continue
|
||||||
|
texts.append(normalized)
|
||||||
|
seen.add(lower)
|
||||||
|
return texts
|
||||||
|
|
||||||
|
|
||||||
|
def action_context_text(axis_values: Any) -> str:
|
||||||
|
return " ".join(
|
||||||
|
axis_value_texts(
|
||||||
|
axis_values,
|
||||||
|
priority=ACTION_CONTEXT_PRIORITY,
|
||||||
|
include_unprioritized=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def context_text(*parts: Any, axis_values: Any = None) -> str:
|
||||||
|
text_parts = [clean_text(part) for part in parts if clean_text(part)]
|
||||||
|
text_parts.extend(axis_value_texts(axis_values))
|
||||||
|
return " ".join(part.lower() for part in text_parts if part)
|
||||||
|
|
||||||
|
|
||||||
|
def key_text(axis_values: Any, key: str) -> str:
|
||||||
|
if not isinstance(axis_values, dict):
|
||||||
|
return ""
|
||||||
|
values = value_texts(axis_values.get(key))
|
||||||
|
return values[0].lower() if values else ""
|
||||||
|
|
||||||
|
|
||||||
|
def row_axis_value_texts(
|
||||||
|
row: dict[str, Any],
|
||||||
|
*,
|
||||||
|
skip_keys: set[str] | frozenset[str] | tuple[str, ...] = (),
|
||||||
|
existing_text: Any = "",
|
||||||
|
) -> list[str]:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
return []
|
||||||
|
return axis_value_texts(row.get("item_axis_values"), skip_keys=skip_keys, existing_text=existing_text)
|
||||||
+9
-24
@@ -3,6 +3,11 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
HARDCORE_DETAIL_DENSITY_CHOICES = {"compact", "balanced", "dense"}
|
HARDCORE_DETAIL_DENSITY_CHOICES = {"compact", "balanced", "dense"}
|
||||||
|
|
||||||
@@ -21,28 +26,7 @@ def normalize_hardcore_detail_density(value: Any) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def axis_values_text(axis_values: Any) -> str:
|
def axis_values_text(axis_values: Any) -> str:
|
||||||
if not isinstance(axis_values, dict):
|
return item_axis_policy.action_context_text(axis_values)
|
||||||
return ""
|
|
||||||
priority = (
|
|
||||||
"position",
|
|
||||||
"body_position",
|
|
||||||
"body_arrangement",
|
|
||||||
"arrangement",
|
|
||||||
"angle",
|
|
||||||
"surface",
|
|
||||||
"body_contact",
|
|
||||||
"leg_detail",
|
|
||||||
"oral_act",
|
|
||||||
"oral_detail",
|
|
||||||
"penetration_act",
|
|
||||||
"penetration_detail",
|
|
||||||
"anal_act",
|
|
||||||
"double_act",
|
|
||||||
"threesome_act",
|
|
||||||
"group_act",
|
|
||||||
)
|
|
||||||
parts = [_clean(axis_values.get(key)) for key in priority if _clean(axis_values.get(key))]
|
|
||||||
return " ".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def position_context_text(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
|
def position_context_text(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
|
||||||
@@ -264,14 +248,15 @@ def is_vaginal_penetration_text(*parts: Any) -> bool:
|
|||||||
|
|
||||||
def is_toy_assisted_double_text(*parts: Any) -> bool:
|
def is_toy_assisted_double_text(*parts: Any) -> bool:
|
||||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||||
if "toy" not in text:
|
|
||||||
return False
|
|
||||||
return any(
|
return any(
|
||||||
token in text
|
token in text
|
||||||
for token in (
|
for token in (
|
||||||
"double penetration",
|
"double penetration",
|
||||||
"double-penetration",
|
"double-penetration",
|
||||||
|
"front-and-back double",
|
||||||
"vaginal and anal penetration",
|
"vaginal and anal penetration",
|
||||||
|
"pussy and ass filled",
|
||||||
|
"one penis in pussy and one penis in ass",
|
||||||
"second penetration point",
|
"second penetration point",
|
||||||
"second point of contact",
|
"second point of contact",
|
||||||
"second contact",
|
"second contact",
|
||||||
|
|||||||
+105
-5
@@ -4,12 +4,14 @@ import re
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from . import outercourse_action_policy as outercourse_policy
|
||||||
from .krea_action_context import (
|
from .krea_action_context import (
|
||||||
is_close_foreplay_text,
|
is_close_foreplay_text,
|
||||||
position_context_text,
|
position_context_text,
|
||||||
)
|
)
|
||||||
from .krea_detail import detail_clauses, join_detail_clauses
|
from .krea_detail import detail_clauses, join_detail_clauses
|
||||||
except ImportError: # Allows local smoke tests with `python -c`.
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
|
import outercourse_action_policy as outercourse_policy
|
||||||
from krea_action_context import (
|
from krea_action_context import (
|
||||||
is_close_foreplay_text,
|
is_close_foreplay_text,
|
||||||
position_context_text,
|
position_context_text,
|
||||||
@@ -25,8 +27,41 @@ def _clean(value: Any) -> str:
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def sanitize_foreplay_detail(detail: str, role_graph: str = "", composition: str = "") -> str:
|
def strip_redundant_position_detail(detail: str) -> str:
|
||||||
detail = _clean(detail)
|
detail = _clean(detail)
|
||||||
|
if not detail:
|
||||||
|
return ""
|
||||||
|
detail = re.sub(
|
||||||
|
r"^\s*[^,;]*?\bposition\b\s+(?:while|featuring|with)\s+",
|
||||||
|
"",
|
||||||
|
detail,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
detail = re.sub(
|
||||||
|
r"^\s*[^,;]*?\bposition\b,\s*",
|
||||||
|
"",
|
||||||
|
detail,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
detail = re.sub(
|
||||||
|
r"\s+\bin\s+[^,;]*?\bposition\b",
|
||||||
|
"",
|
||||||
|
detail,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
detail = re.sub(
|
||||||
|
r"\s+\bfrom\s+[^,;]*?\bposition\b",
|
||||||
|
"",
|
||||||
|
detail,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
detail = re.sub(r"\s*,\s*", ", ", detail)
|
||||||
|
detail = re.sub(r",\s*,", ",", detail)
|
||||||
|
return _clean(detail).strip(" ,;")
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_foreplay_detail(detail: str, role_graph: str = "", composition: str = "") -> str:
|
||||||
|
detail = strip_redundant_position_detail(detail)
|
||||||
if not detail:
|
if not detail:
|
||||||
return ""
|
return ""
|
||||||
if not is_close_foreplay_text(role_graph, detail, composition):
|
if not is_close_foreplay_text(role_graph, detail, composition):
|
||||||
@@ -127,7 +162,7 @@ def hardcore_item_detail(hard_item: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def dedupe_anchor_detail(detail: str, anchor: str) -> str:
|
def dedupe_anchor_detail(detail: str, anchor: str) -> str:
|
||||||
detail = _clean(detail)
|
detail = strip_redundant_position_detail(detail)
|
||||||
anchor_lower = anchor.lower()
|
anchor_lower = anchor.lower()
|
||||||
duplicate_phrases = {
|
duplicate_phrases = {
|
||||||
"front-and-back": (r"front-and-back contact",),
|
"front-and-back": (r"front-and-back contact",),
|
||||||
@@ -215,16 +250,21 @@ def dedupe_toy_double_detail(detail: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def dedupe_outercourse_detail(detail: str, role_graph: str, hard_item: str = "", axis_values: Any = None) -> str:
|
def dedupe_outercourse_detail(detail: str, role_graph: str, hard_item: str = "", axis_values: Any = None) -> str:
|
||||||
detail = _clean(detail)
|
detail = strip_redundant_position_detail(detail)
|
||||||
if not detail:
|
if not detail:
|
||||||
return ""
|
return ""
|
||||||
context = position_context_text(role_graph, hard_item, "", axis_values)
|
context = position_context_text(role_graph, hard_item, "", axis_values)
|
||||||
context_lower = context.lower()
|
context_lower = context.lower()
|
||||||
breast_sex = any(term in context_lower for term in ("boobjob", "titjob", "breast sex", "breast-sex"))
|
position_text = ""
|
||||||
|
if isinstance(axis_values, dict):
|
||||||
|
position_text = _clean(axis_values.get("position", "")).lower()
|
||||||
|
action_kind = outercourse_policy.infer_outercourse_action_kind(position_text)
|
||||||
|
if action_kind == outercourse_policy.OUTERCOURSE_GENERIC:
|
||||||
|
action_kind = outercourse_policy.infer_outercourse_action_kind(context_lower)
|
||||||
clauses: list[str] = []
|
clauses: list[str] = []
|
||||||
for clause in detail_clauses(detail):
|
for clause in detail_clauses(detail):
|
||||||
lower = clause.lower()
|
lower = clause.lower()
|
||||||
if breast_sex:
|
if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
|
||||||
if lower in ("penis", "breasts", "mouth clearly visible"):
|
if lower in ("penis", "breasts", "mouth clearly visible"):
|
||||||
continue
|
continue
|
||||||
if any(
|
if any(
|
||||||
@@ -250,6 +290,66 @@ def dedupe_outercourse_detail(detail: str, role_graph: str, hard_item: str = "",
|
|||||||
)
|
)
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
elif action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
|
||||||
|
if any(
|
||||||
|
term in lower
|
||||||
|
for term in (
|
||||||
|
"testicle",
|
||||||
|
"balls licking",
|
||||||
|
"balls-licking",
|
||||||
|
"balls held",
|
||||||
|
"balls close",
|
||||||
|
"balls and mouth",
|
||||||
|
"mouth and tongue",
|
||||||
|
"mouth visible",
|
||||||
|
"mouth contact",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
elif action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING:
|
||||||
|
if any(
|
||||||
|
term in lower
|
||||||
|
for term in (
|
||||||
|
"penis licking",
|
||||||
|
"penis-licking",
|
||||||
|
"tongue along",
|
||||||
|
"tongue licking",
|
||||||
|
"underside of the penis",
|
||||||
|
"tongue contact on the penis",
|
||||||
|
"one hand steadies the base",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
elif action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
|
||||||
|
if any(
|
||||||
|
term in lower
|
||||||
|
for term in (
|
||||||
|
"handjob",
|
||||||
|
"hand job",
|
||||||
|
"hand wrapped",
|
||||||
|
"one hand wrapped",
|
||||||
|
"two-handed",
|
||||||
|
"both hands stroking",
|
||||||
|
"hand and penis centered",
|
||||||
|
"fingers and palm visibly stroking",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
elif action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
|
||||||
|
if any(
|
||||||
|
term in lower
|
||||||
|
for term in (
|
||||||
|
"footjob",
|
||||||
|
"foot job",
|
||||||
|
"both soles",
|
||||||
|
"soles pressing",
|
||||||
|
"soles wrap",
|
||||||
|
"toes curled",
|
||||||
|
"feet and penis",
|
||||||
|
"soles and penis",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
continue
|
||||||
clauses.append(clause)
|
clauses.append(clause)
|
||||||
return join_detail_clauses(clauses)
|
return join_detail_clauses(clauses)
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ try:
|
|||||||
normalize_hardcore_detail_density,
|
normalize_hardcore_detail_density,
|
||||||
)
|
)
|
||||||
from .hardcore_action_metadata import (
|
from .hardcore_action_metadata import (
|
||||||
|
ACTION_ANAL,
|
||||||
ACTION_CLIMAX,
|
ACTION_CLIMAX,
|
||||||
ACTION_FOREPLAY,
|
ACTION_FOREPLAY,
|
||||||
|
ACTION_MANUAL,
|
||||||
ACTION_ORAL,
|
ACTION_ORAL,
|
||||||
ACTION_OUTERCOURSE,
|
ACTION_OUTERCOURSE,
|
||||||
ACTION_PENETRATION,
|
ACTION_PENETRATION,
|
||||||
@@ -41,8 +43,10 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
|||||||
normalize_hardcore_detail_density,
|
normalize_hardcore_detail_density,
|
||||||
)
|
)
|
||||||
from hardcore_action_metadata import (
|
from hardcore_action_metadata import (
|
||||||
|
ACTION_ANAL,
|
||||||
ACTION_CLIMAX,
|
ACTION_CLIMAX,
|
||||||
ACTION_FOREPLAY,
|
ACTION_FOREPLAY,
|
||||||
|
ACTION_MANUAL,
|
||||||
ACTION_ORAL,
|
ACTION_ORAL,
|
||||||
ACTION_OUTERCOURSE,
|
ACTION_OUTERCOURSE,
|
||||||
ACTION_PENETRATION,
|
ACTION_PENETRATION,
|
||||||
@@ -145,7 +149,7 @@ def action_detail_for_family(
|
|||||||
) -> tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
if family == ACTION_CLIMAX:
|
if family == ACTION_CLIMAX:
|
||||||
return "", dedupe_climax_detail(detail, role_graph, detail_density)
|
return "", dedupe_climax_detail(detail, role_graph, detail_density)
|
||||||
if family == ACTION_FOREPLAY:
|
if family in (ACTION_FOREPLAY, ACTION_MANUAL):
|
||||||
detail = sanitize_foreplay_detail(detail, role_graph, composition)
|
detail = sanitize_foreplay_detail(detail, role_graph, composition)
|
||||||
return "", limit_detail_for_density(detail, detail_density, False)
|
return "", limit_detail_for_density(detail, detail_density, False)
|
||||||
if family == ACTION_OUTERCOURSE:
|
if family == ACTION_OUTERCOURSE:
|
||||||
@@ -154,7 +158,7 @@ def action_detail_for_family(
|
|||||||
if family == ACTION_ORAL and role_graph:
|
if family == ACTION_ORAL and role_graph:
|
||||||
detail = dedupe_oral_detail(detail, role_graph, hard_item, axis_values)
|
detail = dedupe_oral_detail(detail, role_graph, hard_item, axis_values)
|
||||||
return "", limit_detail_for_density(detail, detail_density, False)
|
return "", limit_detail_for_density(detail, detail_density, False)
|
||||||
if family == ACTION_PENETRATION and role_graph:
|
if family in (ACTION_ANAL, ACTION_PENETRATION) and role_graph:
|
||||||
detail = dedupe_penetration_detail(detail, role_graph, hard_item, axis_values)
|
detail = dedupe_penetration_detail(detail, role_graph, hard_item, axis_values)
|
||||||
return "", limit_detail_for_density(detail, detail_density, False)
|
return "", limit_detail_for_density(detail, detail_density, False)
|
||||||
|
|
||||||
|
|||||||
+20
-13
@@ -4,6 +4,7 @@ import re
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from . import outercourse_action_policy as outercourse_policy
|
||||||
from .krea_action_context import (
|
from .krea_action_context import (
|
||||||
axis_values_text,
|
axis_values_text,
|
||||||
is_close_foreplay_text,
|
is_close_foreplay_text,
|
||||||
@@ -13,6 +14,7 @@ try:
|
|||||||
position_context_text,
|
position_context_text,
|
||||||
)
|
)
|
||||||
except ImportError: # Allows local smoke tests with `python -c`.
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
|
import outercourse_action_policy as outercourse_policy
|
||||||
from krea_action_context import (
|
from krea_action_context import (
|
||||||
axis_values_text,
|
axis_values_text,
|
||||||
is_close_foreplay_text,
|
is_close_foreplay_text,
|
||||||
@@ -51,33 +53,38 @@ def hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "",
|
|||||||
if is_foreplay_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
if is_foreplay_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
||||||
return ""
|
return ""
|
||||||
if is_outercourse_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
if is_outercourse_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
||||||
if any(term in text for term in ("boobjob", "titjob", "breast sex", "breast-sex")):
|
action_kind = outercourse_policy.infer_outercourse_action_kind(position_text)
|
||||||
|
if action_kind == outercourse_policy.OUTERCOURSE_GENERIC:
|
||||||
|
action_kind = outercourse_policy.infer_outercourse_action_kind(text)
|
||||||
|
if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
|
||||||
return "breast-sex outercourse pose"
|
return "breast-sex outercourse pose"
|
||||||
if any(term in text for term in ("testicle", "balls licking", "balls-licking", "balls and mouth")):
|
if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
|
||||||
return "testicle-sucking outercourse pose"
|
return "testicle-sucking outercourse pose"
|
||||||
if any(term in text for term in ("penis licking", "penis-licking", "tongue along", "tongue licking")):
|
if action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING:
|
||||||
return "penis-licking outercourse pose"
|
return "penis-licking outercourse pose"
|
||||||
if any(term in text for term in ("handjob", "hand job", "hand wrapped", "hand stroking", "manual stimulation")):
|
if action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
|
||||||
return "handjob outercourse pose"
|
return "handjob outercourse pose"
|
||||||
if any(term in text for term in ("footjob", "soles", "toes curled", "feet stroking")):
|
if action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
|
||||||
return "footjob outercourse pose"
|
return "footjob outercourse pose"
|
||||||
return "non-penetrative outercourse pose"
|
return "non-penetrative outercourse pose"
|
||||||
if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
||||||
|
prefix = "toy-assisted " if ("toy" in text or "strap-on" in text) else ""
|
||||||
|
front_back = "front-and-back" in text or "from the front" in text or "pussy and ass" in text
|
||||||
if "face-down ass-up" in text or "face-down" in text:
|
if "face-down ass-up" in text or "face-down" in text:
|
||||||
return "toy-assisted face-down rear-entry double-penetration pose"
|
return f"{prefix}face-down rear-entry double-penetration pose"
|
||||||
if "doggy style" in text or "doggy-style" in text or "all fours" in text or "rear-entry" in text:
|
if "doggy style" in text or "doggy-style" in text or "all fours" in text or "rear-entry" in text:
|
||||||
return "toy-assisted rear-entry double-penetration pose"
|
return f"{prefix}rear-entry double-penetration pose"
|
||||||
if "bent-over" in text or "bent forward" in text:
|
if "bent-over" in text or "bent forward" in text:
|
||||||
return "toy-assisted bent-over double-penetration pose"
|
return f"{prefix}bent-over double-penetration pose"
|
||||||
if "spooning anal" in text or "side-lying anal" in text or "side-lying" in text:
|
if "spooning anal" in text or "side-lying anal" in text or "side-lying" in text:
|
||||||
return "toy-assisted side-lying double-penetration pose"
|
return f"{prefix}side-lying double-penetration pose"
|
||||||
if "edge-supported" in text or "bed-edge" in text or "edge-of-bed" in text:
|
if "edge-supported" in text or "bed-edge" in text or "edge-of-bed" in text:
|
||||||
return "toy-assisted edge-supported double-penetration pose"
|
return f"{prefix}edge-supported front-and-back double-penetration pose" if front_back else f"{prefix}edge-supported double-penetration pose"
|
||||||
if "standing anal" in text or "standing supported" in text or "standing" in text:
|
if "standing anal" in text or "standing supported" in text or "standing" in text:
|
||||||
return "toy-assisted standing double-penetration pose"
|
return f"{prefix}standing front-and-back double-penetration pose" if front_back else f"{prefix}standing double-penetration pose"
|
||||||
if "kneeling anal" in text or "kneeling" in text:
|
if "kneeling anal" in text or "kneeling" in text:
|
||||||
return "toy-assisted kneeling rear-entry double-penetration pose"
|
return f"{prefix}kneeling front-and-back double-penetration pose" if front_back else f"{prefix}kneeling rear-entry double-penetration pose"
|
||||||
return "toy-assisted rear-entry double-penetration pose"
|
return f"{prefix}front-and-back double-penetration pose" if front_back else f"{prefix}rear-entry double-penetration pose"
|
||||||
if "double penetration" in text or "vaginal and anal penetration" in text or "front-and-back" in text:
|
if "double penetration" in text or "vaginal and anal penetration" in text or "front-and-back" in text:
|
||||||
if "face-down ass-up" in text:
|
if "face-down ass-up" in text:
|
||||||
return "face-down rear-entry double-penetration pose"
|
return "face-down rear-entry double-penetration pose"
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class KreaConfiguredCastPrompt:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class KreaConfiguredCastDependencies:
|
class KreaConfiguredCastDependencies:
|
||||||
clean: Callable[[Any], str]
|
clean: Callable[[Any], str]
|
||||||
prompt_field: Callable[[str, str], str]
|
|
||||||
sanitize_hardcore_environment_anchors: Callable[[Any], str]
|
sanitize_hardcore_environment_anchors: Callable[[Any], str]
|
||||||
sanitize_hardcore_axis_values: Callable[[Any], Any]
|
sanitize_hardcore_axis_values: Callable[[Any], Any]
|
||||||
sanitize_scene_text_for_cast: Callable[[Any, list[str]], str]
|
sanitize_scene_text_for_cast: Callable[[Any, list[str]], str]
|
||||||
@@ -64,11 +63,7 @@ def format_configured_cast_result(
|
|||||||
men_count = int(row.get("men_count") or 0)
|
men_count = int(row.get("men_count") or 0)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
women_count = men_count = 0
|
women_count = men_count = 0
|
||||||
cast_descriptor_text = (
|
cast_descriptor_text = deps.clean(row.get("cast_descriptor_text"))
|
||||||
deps.clean(row.get("cast_descriptor_text"))
|
|
||||||
or deps.prompt_field(deps.clean(row.get("prompt")), "Characters")
|
|
||||||
or deps.prompt_field(deps.clean(row.get("prompt")), "Cast descriptors")
|
|
||||||
)
|
|
||||||
pov_labels = deps.pov_labels_from_value(row.get("pov_character_labels"))
|
pov_labels = deps.pov_labels_from_value(row.get("pov_character_labels"))
|
||||||
camera = request.camera
|
camera = request.camera
|
||||||
if pov_labels:
|
if pov_labels:
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import formatter_detail as detail_policy
|
||||||
|
from . import formatter_input as input_policy
|
||||||
|
from . import formatter_route_trace as trace_policy
|
||||||
|
from . import formatter_target as target_policy
|
||||||
|
except ImportError: # pragma: no cover - plain-script smoke tests
|
||||||
|
import formatter_detail as detail_policy
|
||||||
|
import formatter_input as input_policy
|
||||||
|
import formatter_route_trace as trace_policy
|
||||||
|
import formatter_target as target_policy
|
||||||
|
|
||||||
|
|
||||||
|
STYLE_MODES = ("preserve", "photographic", "minimal")
|
||||||
|
DEFAULT_STYLE_MODE = "preserve"
|
||||||
|
|
||||||
|
|
||||||
|
def style_mode_choices() -> list[str]:
|
||||||
|
return list(STYLE_MODES)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_style_mode(value: Any) -> str:
|
||||||
|
mode = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
||||||
|
return mode if mode in STYLE_MODES else DEFAULT_STYLE_MODE
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class KreaFormatRequest:
|
||||||
|
source_text: str
|
||||||
|
metadata_json: str = ""
|
||||||
|
negative_prompt: str = ""
|
||||||
|
input_hint: str = "auto"
|
||||||
|
target: str = "auto"
|
||||||
|
detail_level: str = "balanced"
|
||||||
|
style_mode: str = "preserve"
|
||||||
|
preserve_trigger: bool = False
|
||||||
|
extra_positive: str = ""
|
||||||
|
extra_negative: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class KreaFormatRoute:
|
||||||
|
output: dict[str, str]
|
||||||
|
branch: str
|
||||||
|
method: str
|
||||||
|
target: str
|
||||||
|
detail_level: str
|
||||||
|
style_mode: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class KreaFormatDependencies:
|
||||||
|
trigger_candidates: tuple[str, ...]
|
||||||
|
clean: Callable[[Any], str]
|
||||||
|
row_from_inputs: Callable[[str, str, str], tuple[dict[str, Any] | None, str]]
|
||||||
|
normal_row_to_krea: Callable[[dict[str, Any], str, str], tuple[str, str]]
|
||||||
|
insta_pair_to_krea: Callable[[dict[str, Any], str, str], tuple[str, str, str, str]]
|
||||||
|
fallback_text_to_krea: Callable[[str, bool, str, str], tuple[str, str, str]]
|
||||||
|
append_formatter_hints: Callable[..., str]
|
||||||
|
combine_negative: Callable[..., str]
|
||||||
|
sanitize_prose_text: Callable[..., str]
|
||||||
|
sanitize_negative_text: Callable[[str], str]
|
||||||
|
|
||||||
|
|
||||||
|
def format_krea2_prompt_result(request: KreaFormatRequest, deps: KreaFormatDependencies) -> KreaFormatRoute:
|
||||||
|
detail_level = detail_policy.normalize_detail_level(request.detail_level)
|
||||||
|
style_mode = normalize_style_mode(request.style_mode)
|
||||||
|
target = target_policy.normalize_target(request.target)
|
||||||
|
input_hint = input_policy.normalize_input_hint(request.input_hint, text_hint=input_policy.INPUT_HINT_PROMPT)
|
||||||
|
row, method = deps.row_from_inputs(request.source_text, request.metadata_json, request.input_hint)
|
||||||
|
|
||||||
|
if row and input_policy.is_pair_metadata(row):
|
||||||
|
pair_target = target_policy.pair_policy(target)
|
||||||
|
soft_prompt, soft_negative, hard_prompt, hard_negative = deps.insta_pair_to_krea(
|
||||||
|
row,
|
||||||
|
detail_level,
|
||||||
|
style_mode,
|
||||||
|
)
|
||||||
|
soft_row = row.get("softcore_row") if isinstance(row.get("softcore_row"), dict) else {}
|
||||||
|
hard_row = row.get("hardcore_row") if isinstance(row.get("hardcore_row"), dict) else {}
|
||||||
|
soft_prompt = deps.append_formatter_hints(soft_prompt, row, soft_row)
|
||||||
|
hard_prompt = deps.append_formatter_hints(hard_prompt, row, hard_row)
|
||||||
|
if request.extra_positive.strip():
|
||||||
|
soft_prompt = f"{soft_prompt.rstrip()} {request.extra_positive.strip()}"
|
||||||
|
hard_prompt = f"{hard_prompt.rstrip()} {request.extra_positive.strip()}"
|
||||||
|
soft_prompt = deps.sanitize_prose_text(soft_prompt, triggers=deps.trigger_candidates)
|
||||||
|
hard_prompt = deps.sanitize_prose_text(hard_prompt, triggers=deps.trigger_candidates)
|
||||||
|
selected = hard_prompt if pair_target.selected_side == "hardcore" else soft_prompt
|
||||||
|
selected_negative = hard_negative if pair_target.selected_side == "hardcore" else soft_negative
|
||||||
|
negative = deps.sanitize_negative_text(
|
||||||
|
deps.combine_negative(selected_negative, request.negative_prompt, request.extra_negative)
|
||||||
|
)
|
||||||
|
output = {
|
||||||
|
"krea_prompt": selected,
|
||||||
|
"negative_prompt": negative,
|
||||||
|
"krea_softcore_prompt": soft_prompt,
|
||||||
|
"krea_hardcore_prompt": hard_prompt,
|
||||||
|
"softcore_negative_prompt": deps.sanitize_negative_text(
|
||||||
|
deps.combine_negative(soft_negative, request.extra_negative)
|
||||||
|
),
|
||||||
|
"hardcore_negative_prompt": deps.sanitize_negative_text(
|
||||||
|
deps.combine_negative(hard_negative, request.extra_negative)
|
||||||
|
),
|
||||||
|
"method": f"{method}:krea2(insta_of_pair)",
|
||||||
|
}
|
||||||
|
output["route_trace_json"] = trace_policy.route_trace_json(
|
||||||
|
formatter="krea2",
|
||||||
|
branch="insta_of_pair",
|
||||||
|
method=output["method"],
|
||||||
|
input_hint=input_hint,
|
||||||
|
target=target,
|
||||||
|
detail_level=detail_level,
|
||||||
|
style_mode=style_mode,
|
||||||
|
**trace_policy.metadata_trace_fields(row, target=target, selected_side=pair_target.selected_side),
|
||||||
|
)
|
||||||
|
return KreaFormatRoute(
|
||||||
|
output=output,
|
||||||
|
branch="insta_of_pair",
|
||||||
|
method=output["method"],
|
||||||
|
target=target,
|
||||||
|
detail_level=detail_level,
|
||||||
|
style_mode=style_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
if row:
|
||||||
|
prompt, kind = deps.normal_row_to_krea(row, detail_level, style_mode)
|
||||||
|
prompt = deps.append_formatter_hints(prompt, row)
|
||||||
|
extracted_negative = deps.clean(row.get("negative_prompt"))
|
||||||
|
method = f"{method}:krea2({kind})"
|
||||||
|
branch = kind
|
||||||
|
else:
|
||||||
|
prompt, extracted_negative, method = deps.fallback_text_to_krea(
|
||||||
|
request.source_text,
|
||||||
|
request.preserve_trigger,
|
||||||
|
detail_level,
|
||||||
|
style_mode,
|
||||||
|
)
|
||||||
|
branch = "fallback"
|
||||||
|
|
||||||
|
if request.extra_positive.strip():
|
||||||
|
prompt = f"{prompt.rstrip()} {request.extra_positive.strip()}"
|
||||||
|
prompt = deps.sanitize_prose_text(prompt, triggers=deps.trigger_candidates)
|
||||||
|
negative = deps.sanitize_negative_text(
|
||||||
|
deps.combine_negative(extracted_negative, request.negative_prompt, request.extra_negative)
|
||||||
|
)
|
||||||
|
output = {
|
||||||
|
"krea_prompt": prompt,
|
||||||
|
"negative_prompt": negative,
|
||||||
|
"krea_softcore_prompt": "",
|
||||||
|
"krea_hardcore_prompt": "",
|
||||||
|
"softcore_negative_prompt": "",
|
||||||
|
"hardcore_negative_prompt": "",
|
||||||
|
"method": method,
|
||||||
|
}
|
||||||
|
output["route_trace_json"] = trace_policy.route_trace_json(
|
||||||
|
formatter="krea2",
|
||||||
|
branch=branch,
|
||||||
|
method=method,
|
||||||
|
input_hint=input_hint,
|
||||||
|
target=target,
|
||||||
|
detail_level=detail_level,
|
||||||
|
style_mode=style_mode,
|
||||||
|
**trace_policy.metadata_trace_fields(row, target=target),
|
||||||
|
)
|
||||||
|
return KreaFormatRoute(
|
||||||
|
output=output,
|
||||||
|
branch=branch,
|
||||||
|
method=method,
|
||||||
|
target=target,
|
||||||
|
detail_level=detail_level,
|
||||||
|
style_mode=style_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_krea2_prompt(request: KreaFormatRequest, deps: KreaFormatDependencies) -> dict[str, str]:
|
||||||
|
return format_krea2_prompt_result(request, deps).output
|
||||||
+66
-64
@@ -5,7 +5,9 @@ from typing import Any
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from . import formatter_input as input_policy
|
from . import formatter_input as input_policy
|
||||||
|
from . import krea_format_route
|
||||||
from . import route_metadata as route_metadata_policy
|
from . import route_metadata as route_metadata_policy
|
||||||
|
from . import softcore_text_policy
|
||||||
from .krea_action_context import (
|
from .krea_action_context import (
|
||||||
is_close_foreplay_text as _is_close_foreplay_text,
|
is_close_foreplay_text as _is_close_foreplay_text,
|
||||||
is_outercourse_text as _is_outercourse_text,
|
is_outercourse_text as _is_outercourse_text,
|
||||||
@@ -37,10 +39,12 @@ try:
|
|||||||
pov_labels_from_value as _pov_labels_from_value,
|
pov_labels_from_value as _pov_labels_from_value,
|
||||||
)
|
)
|
||||||
from .krea_pov_actions import pov_action_phrase as _pov_action_phrase
|
from .krea_pov_actions import pov_action_phrase as _pov_action_phrase
|
||||||
from .prompt_hygiene import sanitize_negative_text, sanitize_prose_text
|
from .prompt_hygiene import combine_negative_text, sanitize_negative_text, sanitize_prose_text
|
||||||
except ImportError: # Allows local smoke tests with `python -c`.
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
import formatter_input as input_policy
|
import formatter_input as input_policy
|
||||||
|
import krea_format_route
|
||||||
import route_metadata as route_metadata_policy
|
import route_metadata as route_metadata_policy
|
||||||
|
import softcore_text_policy
|
||||||
from krea_action_context import (
|
from krea_action_context import (
|
||||||
is_close_foreplay_text as _is_close_foreplay_text,
|
is_close_foreplay_text as _is_close_foreplay_text,
|
||||||
is_outercourse_text as _is_outercourse_text,
|
is_outercourse_text as _is_outercourse_text,
|
||||||
@@ -72,7 +76,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
|||||||
pov_labels_from_value as _pov_labels_from_value,
|
pov_labels_from_value as _pov_labels_from_value,
|
||||||
)
|
)
|
||||||
from krea_pov_actions import pov_action_phrase as _pov_action_phrase
|
from krea_pov_actions import pov_action_phrase as _pov_action_phrase
|
||||||
from prompt_hygiene import sanitize_negative_text, sanitize_prose_text
|
from prompt_hygiene import combine_negative_text, sanitize_negative_text, sanitize_prose_text
|
||||||
|
|
||||||
|
|
||||||
TRIGGER_CANDIDATES = (
|
TRIGGER_CANDIDATES = (
|
||||||
@@ -197,8 +201,7 @@ def _single_caption_front(row: dict[str, Any]) -> dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def _combine_negative(*parts: str) -> str:
|
def _combine_negative(*parts: str) -> str:
|
||||||
cleaned = [_clean(part).strip(" ,.") for part in parts if _clean(part).strip(" ,.")]
|
return combine_negative_text(*parts)
|
||||||
return ", ".join(cleaned)
|
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str:
|
def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str:
|
||||||
@@ -227,6 +230,10 @@ def _composition_phrase(
|
|||||||
detail_density: str = "balanced",
|
detail_density: str = "balanced",
|
||||||
) -> str:
|
) -> str:
|
||||||
composition = _clean(composition)
|
composition = _clean(composition)
|
||||||
|
if not composition:
|
||||||
|
return ""
|
||||||
|
composition = re.sub(r"\s+composition$", "", composition, flags=re.IGNORECASE)
|
||||||
|
composition = re.sub(r"\bcomposition\b", "frame", composition, flags=re.IGNORECASE).strip(" ,")
|
||||||
if not composition:
|
if not composition:
|
||||||
return ""
|
return ""
|
||||||
action_lower = _clean(action).lower()
|
action_lower = _clean(action).lower()
|
||||||
@@ -321,14 +328,30 @@ def _expression_phrase(expression: Any) -> str:
|
|||||||
expression,
|
expression,
|
||||||
flags=re.IGNORECASE,
|
flags=re.IGNORECASE,
|
||||||
):
|
):
|
||||||
return f"Expressions: {expression}"
|
expression = re.sub(
|
||||||
|
r"\b((?:Woman|Man) [A-Z]|the (?:woman|man)) has\b",
|
||||||
|
r"\1 showing",
|
||||||
|
expression,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
return f"With {expression[:1].lower()}{expression[1:]}"
|
||||||
return f"with {expression}"
|
return f"with {expression}"
|
||||||
|
|
||||||
|
|
||||||
|
def _natural_camera_phrase(value: str) -> str:
|
||||||
|
value = _clean(value)
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
value = re.sub(r"^camera:\s*", "", value, flags=re.IGNORECASE).strip()
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
return f"Captured with {value}"
|
||||||
|
|
||||||
|
|
||||||
def _camera_phrase(row: dict[str, Any]) -> str:
|
def _camera_phrase(row: dict[str, Any]) -> str:
|
||||||
directive = _clean(row.get("camera_directive"))
|
directive = _clean(row.get("camera_directive"))
|
||||||
if directive:
|
if directive:
|
||||||
return directive
|
return _natural_camera_phrase(directive)
|
||||||
config = row.get("camera_config")
|
config = row.get("camera_config")
|
||||||
if isinstance(config, dict):
|
if isinstance(config, dict):
|
||||||
detail = _clean(config.get("camera_detail"))
|
detail = _clean(config.get("camera_detail"))
|
||||||
@@ -338,13 +361,13 @@ def _camera_phrase(row: dict[str, Any]) -> str:
|
|||||||
if custom:
|
if custom:
|
||||||
base = _clean(config.get("camera_mode")).replace("_", " ")
|
base = _clean(config.get("camera_mode")).replace("_", " ")
|
||||||
pieces = [piece for piece in (base, custom) if piece and piece != "standard"]
|
pieces = [piece for piece in (base, custom) if piece and piece != "standard"]
|
||||||
return "Camera: " + ", ".join(pieces)
|
return _natural_camera_phrase(", ".join(pieces))
|
||||||
mode = _clean(config.get("camera_mode")).replace("_", " ")
|
mode = _clean(config.get("camera_mode")).replace("_", " ")
|
||||||
shot = _clean(config.get("shot_size")).replace("_", " ")
|
shot = _clean(config.get("shot_size")).replace("_", " ")
|
||||||
angle = _clean(config.get("angle")).replace("_", " ")
|
angle = _clean(config.get("angle")).replace("_", " ")
|
||||||
pieces = [piece for piece in (mode, shot, angle) if piece and piece != "auto" and piece != "standard"]
|
pieces = [piece for piece in (mode, shot, angle) if piece and piece != "auto" and piece != "standard"]
|
||||||
if pieces:
|
if pieces:
|
||||||
return "Camera: " + ", ".join(pieces)
|
return _natural_camera_phrase(", ".join(pieces))
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@@ -362,7 +385,7 @@ def _camera_phrase_from_config(config: Any) -> str:
|
|||||||
if custom:
|
if custom:
|
||||||
base = _clean(config.get("camera_mode")).replace("_", " ")
|
base = _clean(config.get("camera_mode")).replace("_", " ")
|
||||||
pieces = [piece for piece in (base, custom) if piece and piece != "standard"]
|
pieces = [piece for piece in (base, custom) if piece and piece != "standard"]
|
||||||
return "Camera: " + ", ".join(pieces)
|
return _natural_camera_phrase(", ".join(pieces))
|
||||||
values = [
|
values = [
|
||||||
_clean(config.get("camera_mode")).replace("_", " "),
|
_clean(config.get("camera_mode")).replace("_", " "),
|
||||||
_clean(config.get("shot_size")).replace("_", " "),
|
_clean(config.get("shot_size")).replace("_", " "),
|
||||||
@@ -375,13 +398,13 @@ def _camera_phrase_from_config(config: Any) -> str:
|
|||||||
pieces = [value for value in values if value and value not in ("auto", "standard")]
|
pieces = [value for value in values if value and value not in ("auto", "standard")]
|
||||||
if not pieces:
|
if not pieces:
|
||||||
return ""
|
return ""
|
||||||
return "Camera: " + ", ".join(pieces)
|
return _natural_camera_phrase(", ".join(pieces))
|
||||||
|
|
||||||
|
|
||||||
def _pair_camera_phrase(directive: Any, config: Any, row: dict[str, Any]) -> str:
|
def _pair_camera_phrase(directive: Any, config: Any, row: dict[str, Any]) -> str:
|
||||||
directive_text = _clean(directive)
|
directive_text = _clean(directive)
|
||||||
if directive_text:
|
if directive_text:
|
||||||
return directive_text
|
return _natural_camera_phrase(directive_text)
|
||||||
if isinstance(config, dict) and (
|
if isinstance(config, dict) and (
|
||||||
_clean(config.get("camera_detail")) == "off" or _clean(config.get("camera_mode")) == "disabled"
|
_clean(config.get("camera_detail")) == "off" or _clean(config.get("camera_mode")) == "disabled"
|
||||||
):
|
):
|
||||||
@@ -395,7 +418,7 @@ def _style_phrase(row: dict[str, Any], style_mode: str) -> str:
|
|||||||
if style_mode == "photographic":
|
if style_mode == "photographic":
|
||||||
return "realistic creator-shot photography with natural lighting, tactile skin and fabric detail, and clean social-media composition"
|
return "realistic creator-shot photography with natural lighting, tactile skin and fabric detail, and clean social-media composition"
|
||||||
style = _clean(row.get("style"))
|
style = _clean(row.get("style"))
|
||||||
suffix = _clean(row.get("positive_suffix")) or _prompt_field(_clean(row.get("prompt")), "Use")
|
suffix = _clean(row.get("positive_suffix"))
|
||||||
if style and suffix:
|
if style and suffix:
|
||||||
return f"{style}; {suffix}"
|
return f"{style}; {suffix}"
|
||||||
return style or suffix
|
return style or suffix
|
||||||
@@ -454,7 +477,6 @@ def _krea_normal_row_request_from_row(
|
|||||||
def _krea_configured_cast_dependencies() -> krea_configured_cast_formatter.KreaConfiguredCastDependencies:
|
def _krea_configured_cast_dependencies() -> krea_configured_cast_formatter.KreaConfiguredCastDependencies:
|
||||||
return krea_configured_cast_formatter.KreaConfiguredCastDependencies(
|
return krea_configured_cast_formatter.KreaConfiguredCastDependencies(
|
||||||
clean=_clean,
|
clean=_clean,
|
||||||
prompt_field=_prompt_field,
|
|
||||||
sanitize_hardcore_environment_anchors=_sanitize_hardcore_environment_anchors,
|
sanitize_hardcore_environment_anchors=_sanitize_hardcore_environment_anchors,
|
||||||
sanitize_hardcore_axis_values=_sanitize_hardcore_axis_values,
|
sanitize_hardcore_axis_values=_sanitize_hardcore_axis_values,
|
||||||
sanitize_scene_text_for_cast=_sanitize_scene_text_for_cast,
|
sanitize_scene_text_for_cast=_sanitize_scene_text_for_cast,
|
||||||
@@ -569,6 +591,7 @@ def _krea_pair_format_dependencies() -> krea_pair_formatter.KreaPairFormatDepend
|
|||||||
pov_camera_phrase=lambda labels: _pov_camera_phrase(labels),
|
pov_camera_phrase=lambda labels: _pov_camera_phrase(labels),
|
||||||
pov_soft_camera_phrase=lambda labels: _pov_camera_phrase(labels, softcore=True),
|
pov_soft_camera_phrase=lambda labels: _pov_camera_phrase(labels, softcore=True),
|
||||||
pov_composition_text=_pov_composition_text,
|
pov_composition_text=_pov_composition_text,
|
||||||
|
softcore_cast_presence_phrase=softcore_text_policy.softcore_cast_presence_phrase,
|
||||||
natural_clothing_state=_natural_clothing_state,
|
natural_clothing_state=_natural_clothing_state,
|
||||||
composition_phrase=_composition_phrase,
|
composition_phrase=_composition_phrase,
|
||||||
paragraph=_paragraph,
|
paragraph=_paragraph,
|
||||||
@@ -604,6 +627,21 @@ def _fallback_text_to_krea(
|
|||||||
return _paragraph([positive]), negative, "text(fallback)"
|
return _paragraph([positive]), negative, "text(fallback)"
|
||||||
|
|
||||||
|
|
||||||
|
def _krea_format_dependencies() -> krea_format_route.KreaFormatDependencies:
|
||||||
|
return krea_format_route.KreaFormatDependencies(
|
||||||
|
trigger_candidates=TRIGGER_CANDIDATES,
|
||||||
|
clean=_clean,
|
||||||
|
row_from_inputs=_row_from_inputs,
|
||||||
|
normal_row_to_krea=_normal_row_to_krea,
|
||||||
|
insta_pair_to_krea=_insta_pair_to_krea,
|
||||||
|
fallback_text_to_krea=_fallback_text_to_krea,
|
||||||
|
append_formatter_hints=_append_formatter_hints,
|
||||||
|
combine_negative=_combine_negative,
|
||||||
|
sanitize_prose_text=sanitize_prose_text,
|
||||||
|
sanitize_negative_text=sanitize_negative_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_krea2_prompt(
|
def format_krea2_prompt(
|
||||||
source_text: str,
|
source_text: str,
|
||||||
metadata_json: str = "",
|
metadata_json: str = "",
|
||||||
@@ -616,54 +654,18 @@ def format_krea2_prompt(
|
|||||||
extra_positive: str = "",
|
extra_positive: str = "",
|
||||||
extra_negative: str = "",
|
extra_negative: str = "",
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
detail_level = detail_level if detail_level in ("concise", "balanced", "dense") else "balanced"
|
return krea_format_route.format_krea2_prompt(
|
||||||
style_mode = style_mode if style_mode in ("preserve", "photographic", "minimal") else "preserve"
|
krea_format_route.KreaFormatRequest(
|
||||||
target = target if target in ("auto", "single", "softcore", "hardcore") else "auto"
|
source_text=source_text,
|
||||||
row, method = _row_from_inputs(source_text, metadata_json, input_hint)
|
metadata_json=metadata_json,
|
||||||
extracted_negative = ""
|
negative_prompt=negative_prompt,
|
||||||
|
input_hint=input_hint,
|
||||||
if row and row.get("mode") == "Insta/OF":
|
target=target,
|
||||||
soft_prompt, soft_negative, hard_prompt, hard_negative = _insta_pair_to_krea(row, detail_level, style_mode)
|
detail_level=detail_level,
|
||||||
soft_row = row.get("softcore_row") if isinstance(row.get("softcore_row"), dict) else {}
|
style_mode=style_mode,
|
||||||
hard_row = row.get("hardcore_row") if isinstance(row.get("hardcore_row"), dict) else {}
|
preserve_trigger=preserve_trigger,
|
||||||
soft_prompt = _append_formatter_hints(soft_prompt, row, soft_row)
|
extra_positive=extra_positive,
|
||||||
hard_prompt = _append_formatter_hints(hard_prompt, row, hard_row)
|
extra_negative=extra_negative,
|
||||||
if extra_positive.strip():
|
),
|
||||||
soft_prompt = f"{soft_prompt.rstrip()} {extra_positive.strip()}"
|
_krea_format_dependencies(),
|
||||||
hard_prompt = f"{hard_prompt.rstrip()} {extra_positive.strip()}"
|
)
|
||||||
soft_prompt = sanitize_prose_text(soft_prompt, triggers=TRIGGER_CANDIDATES)
|
|
||||||
hard_prompt = sanitize_prose_text(hard_prompt, triggers=TRIGGER_CANDIDATES)
|
|
||||||
selected = hard_prompt if target == "hardcore" else soft_prompt if target == "softcore" else soft_prompt
|
|
||||||
selected_negative = hard_negative if target == "hardcore" else soft_negative
|
|
||||||
negative = sanitize_negative_text(_combine_negative(selected_negative, negative_prompt, extra_negative))
|
|
||||||
return {
|
|
||||||
"krea_prompt": selected,
|
|
||||||
"negative_prompt": negative,
|
|
||||||
"krea_softcore_prompt": soft_prompt,
|
|
||||||
"krea_hardcore_prompt": hard_prompt,
|
|
||||||
"softcore_negative_prompt": sanitize_negative_text(_combine_negative(soft_negative, extra_negative)),
|
|
||||||
"hardcore_negative_prompt": sanitize_negative_text(_combine_negative(hard_negative, extra_negative)),
|
|
||||||
"method": f"{method}:krea2(insta_of_pair)",
|
|
||||||
}
|
|
||||||
|
|
||||||
if row:
|
|
||||||
prompt, kind = _normal_row_to_krea(row, detail_level, style_mode)
|
|
||||||
prompt = _append_formatter_hints(prompt, row)
|
|
||||||
extracted_negative = _clean(row.get("negative_prompt"))
|
|
||||||
method = f"{method}:krea2({kind})"
|
|
||||||
else:
|
|
||||||
prompt, extracted_negative, method = _fallback_text_to_krea(source_text, preserve_trigger, detail_level, style_mode)
|
|
||||||
|
|
||||||
if extra_positive.strip():
|
|
||||||
prompt = f"{prompt.rstrip()} {extra_positive.strip()}"
|
|
||||||
prompt = sanitize_prose_text(prompt, triggers=TRIGGER_CANDIDATES)
|
|
||||||
negative = sanitize_negative_text(_combine_negative(extracted_negative, negative_prompt, extra_negative))
|
|
||||||
return {
|
|
||||||
"krea_prompt": prompt,
|
|
||||||
"negative_prompt": negative,
|
|
||||||
"krea_softcore_prompt": "",
|
|
||||||
"krea_hardcore_prompt": "",
|
|
||||||
"softcore_negative_prompt": "",
|
|
||||||
"hardcore_negative_prompt": "",
|
|
||||||
"method": method,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -54,6 +54,52 @@ def _couple_clothing_phrase(item: str, clean: Callable[[Any], str]) -> str:
|
|||||||
return f"The couple wears {item}"
|
return f"The couple wears {item}"
|
||||||
|
|
||||||
|
|
||||||
|
def _cap_first(text: str) -> str:
|
||||||
|
text = str(text or "").strip()
|
||||||
|
return f"{text[:1].upper()}{text[1:]}" if text else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _couple_subject_phrase(subject: str, ages: str) -> str:
|
||||||
|
subject = _cap_first(subject or "adult couple")
|
||||||
|
ages = str(ages or "").strip()
|
||||||
|
if ages:
|
||||||
|
return f"{subject}, {ages}"
|
||||||
|
return subject
|
||||||
|
|
||||||
|
|
||||||
|
def _framed_composition_phrase(composition: str, prefix: str = "framed as") -> str:
|
||||||
|
composition = re.sub(r"\s+composition$", "", str(composition or "").strip(), flags=re.IGNORECASE)
|
||||||
|
composition = re.sub(
|
||||||
|
r"\bcomposition\b",
|
||||||
|
"frame",
|
||||||
|
composition,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
).strip(" ,")
|
||||||
|
if not composition:
|
||||||
|
return ""
|
||||||
|
return f"{prefix} {composition}"
|
||||||
|
|
||||||
|
|
||||||
|
def _appearance_with_phrase(appearance: str, with_indefinite_article: Callable[[str], str]) -> str:
|
||||||
|
appearance = str(appearance or "").strip()
|
||||||
|
if not appearance:
|
||||||
|
return ""
|
||||||
|
first_clause = appearance.split(",", 1)[0].lower()
|
||||||
|
if re.search(r"\b(?:body|build|figure|frame|physique|silhouette)\b", first_clause):
|
||||||
|
nested_shape = re.match(
|
||||||
|
r"^(.+\b(?:body|build|figure|frame|physique|silhouette))\s+with\s+(.+?)(?=,\s+[^,]*(?:skin|hair|eyes)\b|$)(.*)$",
|
||||||
|
appearance,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if nested_shape:
|
||||||
|
shape = with_indefinite_article(nested_shape.group(1).strip())
|
||||||
|
detail = nested_shape.group(2).strip()
|
||||||
|
rest = nested_shape.group(3).strip()
|
||||||
|
return f"with {shape} defined by {detail}{rest}"
|
||||||
|
appearance = with_indefinite_article(appearance)
|
||||||
|
return f"with {appearance}"
|
||||||
|
|
||||||
|
|
||||||
def format_normal_row_result(
|
def format_normal_row_result(
|
||||||
request: KreaNormalRowRequest,
|
request: KreaNormalRowRequest,
|
||||||
deps: KreaNormalRowDependencies,
|
deps: KreaNormalRowDependencies,
|
||||||
@@ -74,20 +120,23 @@ def format_normal_row_result(
|
|||||||
if primary in ("woman", "man") or subject_type in ("woman", "man", "single_any"):
|
if primary in ("woman", "man") or subject_type in ("woman", "man", "single_any"):
|
||||||
subject = deps.age_subject(row, "adult woman")
|
subject = deps.age_subject(row, "adult woman")
|
||||||
appearance = deps.appearance_phrase(row)
|
appearance = deps.appearance_phrase(row)
|
||||||
|
subject_phrase = deps.with_indefinite_article(subject)
|
||||||
|
appearance_phrase = _appearance_with_phrase(appearance, deps.with_indefinite_article)
|
||||||
|
if appearance_phrase:
|
||||||
|
subject_phrase = f"{subject_phrase} {appearance_phrase}"
|
||||||
parts = [
|
parts = [
|
||||||
deps.with_indefinite_article(subject),
|
subject_phrase,
|
||||||
f"with {appearance}" if appearance else "",
|
|
||||||
f"wearing {item}" if item else "",
|
f"wearing {item}" if item else "",
|
||||||
f"{pose}" if pose else "",
|
f"{pose}" if pose else "",
|
||||||
f"with {expression}" if expression else "",
|
f"with {expression}" if expression else "",
|
||||||
f"in {scene}" if scene else "",
|
f"in {scene}" if scene else "",
|
||||||
camera_scene,
|
camera_scene,
|
||||||
f"framed as {composition}" if composition else "",
|
_framed_composition_phrase(composition),
|
||||||
camera,
|
camera,
|
||||||
style if detail_level != "concise" else "",
|
style if detail_level != "concise" else "",
|
||||||
]
|
]
|
||||||
return KreaNormalRowPrompt(
|
return KreaNormalRowPrompt(
|
||||||
deps.paragraph([", ".join(part for part in parts[:6] if part), *parts[6:]]),
|
deps.paragraph([", ".join(part for part in parts[:5] if part), *parts[5:]]),
|
||||||
"metadata(single)",
|
"metadata(single)",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,15 +147,14 @@ def format_normal_row_result(
|
|||||||
ages = deps.age_detail_phrase(deps.row_value(row, "age", ("Ages",)) or row.get("age_band"))
|
ages = deps.age_detail_phrase(deps.row_value(row, "age", ("Ages",)) or row.get("age_band"))
|
||||||
body = deps.row_value(row, "body", ("Body types",)) or deps.clean(row.get("body_type"))
|
body = deps.row_value(row, "body", ("Body types",)) or deps.clean(row.get("body_type"))
|
||||||
parts = [
|
parts = [
|
||||||
f"An adult couple: {subject}, all visibly adult",
|
_couple_subject_phrase(subject, ages),
|
||||||
f"Age detail: {ages}" if ages else "",
|
|
||||||
f"Body types: {body}" if body else "",
|
f"Body types: {body}" if body else "",
|
||||||
_couple_clothing_phrase(item, deps.clean) if item else "",
|
_couple_clothing_phrase(item, deps.clean) if item else "",
|
||||||
f"The pose is {pose}" if pose else "",
|
f"The pose is {pose}" if pose else "",
|
||||||
f"The setting is {scene}" if scene else "",
|
f"The setting is {scene}" if scene else "",
|
||||||
camera_scene,
|
camera_scene,
|
||||||
f"Facial expressions are {expression}" if expression else "",
|
f"Facial expressions are {expression}" if expression else "",
|
||||||
f"The image is framed as {composition}" if composition else "",
|
_framed_composition_phrase(composition, "The image is framed as"),
|
||||||
camera,
|
camera,
|
||||||
style if detail_level != "concise" else "",
|
style if detail_level != "concise" else "",
|
||||||
]
|
]
|
||||||
@@ -119,7 +167,7 @@ def format_normal_row_result(
|
|||||||
f"in {scene}" if scene else "",
|
f"in {scene}" if scene else "",
|
||||||
camera_scene,
|
camera_scene,
|
||||||
f"with {expression}" if expression else "",
|
f"with {expression}" if expression else "",
|
||||||
f"framed as {composition}" if composition else "",
|
_framed_composition_phrase(composition),
|
||||||
camera,
|
camera,
|
||||||
style if detail_level != "concise" else "",
|
style if detail_level != "concise" else "",
|
||||||
]
|
]
|
||||||
|
|||||||
+9
-13
@@ -47,6 +47,7 @@ class KreaPairFormatDependencies:
|
|||||||
pov_camera_phrase: Callable[[list[str]], str]
|
pov_camera_phrase: Callable[[list[str]], str]
|
||||||
pov_soft_camera_phrase: Callable[[list[str]], str]
|
pov_soft_camera_phrase: Callable[[list[str]], str]
|
||||||
pov_composition_text: Callable[[Any, list[str]], str]
|
pov_composition_text: Callable[[Any, list[str]], str]
|
||||||
|
softcore_cast_presence_phrase: Callable[..., str]
|
||||||
natural_clothing_state: Callable[[Any, str], str]
|
natural_clothing_state: Callable[[Any, str], str]
|
||||||
composition_phrase: Callable[..., str]
|
composition_phrase: Callable[..., str]
|
||||||
paragraph: Callable[[list[str]], str]
|
paragraph: Callable[[list[str]], str]
|
||||||
@@ -76,7 +77,7 @@ def format_insta_pair_result(request: KreaPairFormatRequest, deps: KreaPairForma
|
|||||||
soft_level = deps.clean(options.get("softcore_level")).replace("_", " ")
|
soft_level = deps.clean(options.get("softcore_level")).replace("_", " ")
|
||||||
hard_level = deps.clean(options.get("hardcore_level")).replace("_", " ")
|
hard_level = deps.clean(options.get("hardcore_level")).replace("_", " ")
|
||||||
same_room = options.get("continuity") == "same_creator_same_room"
|
same_room = options.get("continuity") == "same_creator_same_room"
|
||||||
hard_scene = soft.get("scene_text") if same_room and soft.get("scene_text") else hard.get("scene_text")
|
hard_scene = hard.get("scene_text") or (soft.get("scene_text") if same_room else "")
|
||||||
hard_composition = deps.sanitize_hardcore_environment_anchors(hard.get("composition"))
|
hard_composition = deps.sanitize_hardcore_environment_anchors(hard.get("composition"))
|
||||||
hard_source_composition = deps.sanitize_hardcore_environment_anchors(hard.get("source_composition") or hard_composition)
|
hard_source_composition = deps.sanitize_hardcore_environment_anchors(hard.get("source_composition") or hard_composition)
|
||||||
pov_labels = deps.merge_labels(
|
pov_labels = deps.merge_labels(
|
||||||
@@ -134,17 +135,12 @@ def format_insta_pair_result(request: KreaPairFormatRequest, deps: KreaPairForma
|
|||||||
hard_output_composition = deps.pov_composition_text(hard_composition, pov_labels)
|
hard_output_composition = deps.pov_composition_text(hard_composition, pov_labels)
|
||||||
same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"
|
same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"
|
||||||
soft_output_composition = deps.pov_composition_text(soft.get("composition"), pov_labels if same_soft_cast else [])
|
soft_output_composition = deps.pov_composition_text(soft.get("composition"), pov_labels if same_soft_cast else [])
|
||||||
if same_soft_cast and pov_labels:
|
soft_cast_presence = deps.softcore_cast_presence_phrase(
|
||||||
soft_cast_presence = (
|
same_cast=same_soft_cast,
|
||||||
"the woman is framed from the POV participant's first-person camera in a soft creator-teaser pose, "
|
pov_labels=pov_labels if same_soft_cast else [],
|
||||||
"with the POV participant kept off-camera as the viewpoint and implied by camera position or foreground cues"
|
cast_label=deps.label_join(soft_labels),
|
||||||
)
|
woman_label="the woman",
|
||||||
else:
|
)
|
||||||
soft_cast_presence = (
|
|
||||||
f"{deps.label_join(soft_labels)} share the frame in a soft creator-teaser pose"
|
|
||||||
if same_soft_cast
|
|
||||||
else "The image focuses on the woman alone"
|
|
||||||
)
|
|
||||||
partner_styling = row.get("softcore_partner_styling")
|
partner_styling = row.get("softcore_partner_styling")
|
||||||
if isinstance(partner_styling, dict):
|
if isinstance(partner_styling, dict):
|
||||||
outfits = partner_styling.get("outfits")
|
outfits = partner_styling.get("outfits")
|
||||||
@@ -195,7 +191,7 @@ def format_insta_pair_result(request: KreaPairFormatRequest, deps: KreaPairForma
|
|||||||
deps.expression_phrase(soft_expression),
|
deps.expression_phrase(soft_expression),
|
||||||
f"in {soft.get('scene_text')}" if soft.get("scene_text") else "",
|
f"in {soft.get('scene_text')}" if soft.get("scene_text") else "",
|
||||||
soft_camera_scene,
|
soft_camera_scene,
|
||||||
f"framed as {soft_output_composition}" if soft_output_composition else "",
|
deps.composition_phrase(soft_output_composition),
|
||||||
soft_camera,
|
soft_camera,
|
||||||
soft_style if detail_level != "concise" else "",
|
soft_style if detail_level != "concise" else "",
|
||||||
]
|
]
|
||||||
|
|||||||
+211
-31
@@ -4,18 +4,22 @@ import re
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from . import outercourse_action_policy as outercourse_policy
|
||||||
from .krea_action_context import (
|
from .krea_action_context import (
|
||||||
axis_values_text,
|
axis_values_text,
|
||||||
is_climax_text,
|
is_climax_text,
|
||||||
|
is_oral_text,
|
||||||
is_outercourse_text,
|
is_outercourse_text,
|
||||||
is_toy_assisted_double_text,
|
is_toy_assisted_double_text,
|
||||||
position_context_text,
|
position_context_text,
|
||||||
)
|
)
|
||||||
from .krea_detail import limit_detail_for_density
|
from .krea_detail import limit_detail_for_density
|
||||||
except ImportError: # Allows local smoke tests with `python -c`.
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
|
import outercourse_action_policy as outercourse_policy
|
||||||
from krea_action_context import (
|
from krea_action_context import (
|
||||||
axis_values_text,
|
axis_values_text,
|
||||||
is_climax_text,
|
is_climax_text,
|
||||||
|
is_oral_text,
|
||||||
is_outercourse_text,
|
is_outercourse_text,
|
||||||
is_toy_assisted_double_text,
|
is_toy_assisted_double_text,
|
||||||
position_context_text,
|
position_context_text,
|
||||||
@@ -74,12 +78,21 @@ def pov_clean_detail(detail: Any, context: str, detail_density: str) -> str:
|
|||||||
detail = re.sub(r"\bthe POV viewer\b", "the viewer", detail, flags=re.IGNORECASE)
|
detail = re.sub(r"\bthe POV viewer\b", "the viewer", detail, flags=re.IGNORECASE)
|
||||||
detail = re.sub(r"\bthe man's\b", "the viewer's", detail, flags=re.IGNORECASE)
|
detail = re.sub(r"\bthe man's\b", "the viewer's", detail, flags=re.IGNORECASE)
|
||||||
detail = re.sub(r"\bthe man\b", "the viewer", detail, flags=re.IGNORECASE)
|
detail = re.sub(r"\bthe man\b", "the viewer", detail, flags=re.IGNORECASE)
|
||||||
|
detail = re.sub(r"\bhe\b", "the viewer", detail, flags=re.IGNORECASE)
|
||||||
|
detail = re.sub(r"\bhis\b", "the viewer's", detail, flags=re.IGNORECASE)
|
||||||
|
detail = re.sub(r"\bhim\b", "the viewer", detail, flags=re.IGNORECASE)
|
||||||
detail = re.sub(
|
detail = re.sub(
|
||||||
r"^(?:missionary|cowgirl|reverse cowgirl|doggy style|standing sex|spooning sex|edge-supported|edge-of-bed|raised edge|kneeling straddle|lotus sex|bent-over|face-down ass-up|side-lying|kneeling rear-entry)\s+(?:position|pose)\s+(?:featuring|with|while|,)?\s*",
|
r"^(?:missionary|cowgirl|reverse cowgirl|doggy style|standing sex|spooning sex|edge-supported|edge-of-bed|raised edge|kneeling straddle|lotus sex|bent-over|face-down ass-up|side-lying|kneeling rear-entry)\s+(?:position|pose)\s+(?:featuring|with|while|,)?\s*",
|
||||||
"",
|
"",
|
||||||
detail,
|
detail,
|
||||||
flags=re.IGNORECASE,
|
flags=re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
detail = re.sub(
|
||||||
|
r"^(?:kneeling oral|standing oral|chair oral|side-lying oral|sixty-nine|edge-supported oral|edge-of-bed oral|reclining cunnilingus|straddled oral|spread-leg oral)\s+(?:position|pose),?\s*",
|
||||||
|
"",
|
||||||
|
detail,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
detail = re.sub(r"^(?:featuring|with)\s+", "", detail, flags=re.IGNORECASE)
|
detail = re.sub(r"^(?:featuring|with)\s+", "", detail, flags=re.IGNORECASE)
|
||||||
detail = re.sub(
|
detail = re.sub(
|
||||||
r"^(?:full-body|explicit|close-contact|deep|hardcore|vaginal|anal)?\s*(?:penetrative sex|vaginal sex|anal sex|penetration with visible genital contact|hardcore vaginal thrusting|hardcore anal thrusting),?\s*",
|
r"^(?:full-body|explicit|close-contact|deep|hardcore|vaginal|anal)?\s*(?:penetrative sex|vaginal sex|anal sex|penetration with visible genital contact|hardcore vaginal thrusting|hardcore anal thrusting),?\s*",
|
||||||
@@ -125,6 +138,26 @@ def pov_clean_detail(detail: Any, context: str, detail_density: str) -> str:
|
|||||||
return limit_detail_for_density(detail, detail_density, is_climax_text(context, detail))
|
return limit_detail_for_density(detail, detail_density, is_climax_text(context, detail))
|
||||||
|
|
||||||
|
|
||||||
|
def pov_clean_oral_detail(detail: Any, context: str, detail_density: str) -> str:
|
||||||
|
detail = pov_clean_detail(detail, context, detail_density)
|
||||||
|
if not detail:
|
||||||
|
return ""
|
||||||
|
duplicate_patterns = (
|
||||||
|
r"\bthe woman takes the viewer's penis in her mouth with\s+",
|
||||||
|
r"\bthe woman takes the viewer's penis in her mouth\b,?\s*",
|
||||||
|
r"\bher mouth on the viewer's penis\b,?\s*",
|
||||||
|
r"\bthe viewer's mouth on the woman's pussy\b,?\s*",
|
||||||
|
r"\bmouth on the viewer's penis\b,?\s*",
|
||||||
|
r"\bmouth on the woman's pussy\b,?\s*",
|
||||||
|
)
|
||||||
|
for pattern in duplicate_patterns:
|
||||||
|
detail = re.sub(pattern, "", detail, flags=re.IGNORECASE)
|
||||||
|
detail = re.sub(r"\bwith\s+(?=[,;.]|$)", "", detail, flags=re.IGNORECASE)
|
||||||
|
detail = re.sub(r"\s*,\s*", ", ", detail)
|
||||||
|
detail = re.sub(r",\s*,", ",", detail)
|
||||||
|
return _clean(detail).strip(" ,;")
|
||||||
|
|
||||||
|
|
||||||
def pov_hardcore_pose_sentence(
|
def pov_hardcore_pose_sentence(
|
||||||
action: Any,
|
action: Any,
|
||||||
role_graph: Any,
|
role_graph: Any,
|
||||||
@@ -152,6 +185,66 @@ def pov_hardcore_pose_sentence(
|
|||||||
def outercourse_sentence(base: str) -> str:
|
def outercourse_sentence(base: str) -> str:
|
||||||
return _clean(base).rstrip(".")
|
return _clean(base).rstrip(".")
|
||||||
|
|
||||||
|
def oral_sentence(base: str) -> str:
|
||||||
|
details = ""
|
||||||
|
if ";" in action_text:
|
||||||
|
details = pov_clean_oral_detail(action_text.split(";", 1)[1], f"{context} {base}", detail_density)
|
||||||
|
return _clean(f"{base}; {details}" if details else base).rstrip(".")
|
||||||
|
|
||||||
|
def oral_direction() -> tuple[bool, bool]:
|
||||||
|
oral_context = f"{context} {action_lower}"
|
||||||
|
woman_gives = any(
|
||||||
|
token in oral_context
|
||||||
|
for token in (
|
||||||
|
"fellatio",
|
||||||
|
"blowjob",
|
||||||
|
"deepthroat",
|
||||||
|
"penis sucking",
|
||||||
|
"penis in her mouth",
|
||||||
|
"mouth on the viewer's penis",
|
||||||
|
"mouth on viewer's penis",
|
||||||
|
"takes the viewer's penis",
|
||||||
|
"takes the man's penis",
|
||||||
|
"mouth at penis level",
|
||||||
|
"lips wrapped",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
man_gives = any(
|
||||||
|
token in oral_context
|
||||||
|
for token in (
|
||||||
|
"cunnilingus",
|
||||||
|
"pussy licking",
|
||||||
|
"mouth on the woman's pussy",
|
||||||
|
"mouth on her pussy",
|
||||||
|
"mouth pressed to her pussy",
|
||||||
|
"tongue on pussy",
|
||||||
|
"face-sitting",
|
||||||
|
"straddles his face",
|
||||||
|
"straddling the viewer's face",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if "sixty-nine" in oral_context:
|
||||||
|
return True, True
|
||||||
|
return woman_gives, man_gives
|
||||||
|
|
||||||
|
penetrative_tokens = (
|
||||||
|
"penetrat",
|
||||||
|
"thrust",
|
||||||
|
"anal",
|
||||||
|
"cowgirl",
|
||||||
|
"missionary",
|
||||||
|
"doggy",
|
||||||
|
"rear-entry",
|
||||||
|
"spooning",
|
||||||
|
"bent-over",
|
||||||
|
"face-down",
|
||||||
|
"ejaculat",
|
||||||
|
"semen",
|
||||||
|
"cumshot",
|
||||||
|
"climax",
|
||||||
|
)
|
||||||
|
has_penetrative_context = any(token in context or token in action_lower for token in penetrative_tokens)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
"face-sitting" in context
|
"face-sitting" in context
|
||||||
or "face sitting" in context
|
or "face sitting" in context
|
||||||
@@ -163,27 +256,33 @@ def pov_hardcore_pose_sentence(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if is_outercourse_text(context, action_lower):
|
if is_outercourse_text(context, action_lower):
|
||||||
if any(term in context for term in ("boobjob", "titjob", "breast sex", "breast-sex")):
|
action_kind = outercourse_policy.infer_outercourse_action_kind(position_text)
|
||||||
|
if action_kind == outercourse_policy.OUTERCOURSE_GENERIC:
|
||||||
|
action_kind = outercourse_policy.infer_outercourse_action_kind(context, action_lower)
|
||||||
|
if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
|
||||||
return outercourse_sentence(
|
return outercourse_sentence(
|
||||||
"The woman kneels between the viewer's open thighs with her torso bent forward over his pelvis and shoulders low; "
|
"The woman kneels low between the viewer's open thighs with her torso bent forward over his pelvis; "
|
||||||
"both hands lift and press her breasts tightly around the viewer's penis shaft in the lower foreground, with the glans just below her lips"
|
"both hands push her breasts inward around the viewer's penis in the lower foreground, the penis held between her breasts, "
|
||||||
|
"with her chin and lips directly above the glans at the tip"
|
||||||
)
|
)
|
||||||
if any(term in context for term in ("testicle", "balls licking", "balls-licking", "balls and mouth")):
|
if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
|
||||||
return outercourse_sentence(
|
return outercourse_sentence(
|
||||||
"The woman kneels very low between the viewer's open thighs with her torso bent forward and shoulders between his knees; "
|
"The woman bends forward and kneels very low between the viewer's open thighs with her shoulders between his knees; "
|
||||||
"her head is tucked under the penis shaft at the base of the penis, mouth and tongue licking the viewer's balls while his penis points upward above her face in the lower foreground"
|
"her face is below the viewer's penis at testicle height, mouth and tongue licking the viewer's balls while his penis points upward in the lower foreground above her forehead"
|
||||||
)
|
)
|
||||||
if any(term in context for term in ("penis licking", "penis-licking", "tongue along", "tongue licking")):
|
if action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING:
|
||||||
return outercourse_sentence(
|
return outercourse_sentence(
|
||||||
"The woman bends forward between the viewer's open thighs, head low under the viewer's penis with her face directly under the penis; "
|
"The woman bends forward between the viewer's open thighs with her head low under the viewer's penis; "
|
||||||
"her tongue runs along the underside from the penis shaft to the glans while one hand steadies the base of the penis in the lower foreground"
|
"her face is just under the penis while her tongue touches the underside from the base toward the glans at the tip, "
|
||||||
|
"one hand steadying the base of the viewer's penis in the lower foreground"
|
||||||
)
|
)
|
||||||
if any(term in context for term in ("handjob", "hand job", "hand wrapped", "hand stroking", "manual stimulation")):
|
if action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
|
||||||
return outercourse_sentence(
|
return outercourse_sentence(
|
||||||
"The woman kneels between the viewer's open thighs with her torso leaning forward and face visible behind the penis shaft; "
|
"The woman kneels between the viewer's open thighs with her torso leaning forward and face visible behind the viewer's penis; "
|
||||||
"one hand wraps around the penis shaft in the lower foreground while the other hand steadies the base of the penis as she strokes toward the glans"
|
"one hand grips and strokes the viewer's penis in the lower foreground while the other hand steadies its base, "
|
||||||
|
"thumb and fingers visible around the penis as she strokes toward the glans"
|
||||||
)
|
)
|
||||||
if any(term in context for term in ("footjob", "soles", "toes curled", "feet stroking")):
|
if action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
|
||||||
return outercourse_sentence(
|
return outercourse_sentence(
|
||||||
"The woman faces the viewer with her hips back, torso visible behind her raised legs, and both knees bent open toward the camera; "
|
"The woman faces the viewer with her hips back, torso visible behind her raised legs, and both knees bent open toward the camera; "
|
||||||
"her soles wrap around the penis shaft in the lower foreground, toes curled around the penis shaft with her face visible beyond her feet"
|
"her soles wrap around the penis shaft in the lower foreground, toes curled around the penis shaft with her face visible beyond her feet"
|
||||||
@@ -191,24 +290,105 @@ def pov_hardcore_pose_sentence(
|
|||||||
return outercourse_sentence(
|
return outercourse_sentence(
|
||||||
"The woman stays close to the viewer's pelvis, keeping the non-penetrative contact centered in the lower foreground with her face visible behind the contact"
|
"The woman stays close to the viewer's pelvis, keeping the non-penetrative contact centered in the lower foreground with her face visible behind the contact"
|
||||||
)
|
)
|
||||||
penetrative_tokens = (
|
|
||||||
"penetrat",
|
if is_oral_text(context, action_lower) and not has_penetrative_context:
|
||||||
"thrust",
|
woman_gives, man_gives = oral_direction()
|
||||||
"anal",
|
if "sixty-nine" in position_context:
|
||||||
"cowgirl",
|
return oral_sentence(
|
||||||
"missionary",
|
"POV sixty-nine oral position: the woman lies head-to-hips over the viewer, her pelvis close to his face and her head lowered toward his hips; "
|
||||||
"doggy",
|
"her mouth on the viewer's penis and the viewer's mouth on the woman's pussy, with her torso, hips, mouth, and the viewer's lower-foreground body cues aligned in one first-person frame"
|
||||||
"rear-entry",
|
)
|
||||||
"spooning",
|
if "side-lying oral" in position_context or "side lying oral" in position_context:
|
||||||
"side-lying",
|
if woman_gives and not man_gives:
|
||||||
"bent-over",
|
return oral_sentence(
|
||||||
"face-down",
|
"POV side-lying oral position: the viewer lies on his side with hips angled toward the woman while she lies beside his thighs; "
|
||||||
"ejaculat",
|
"her head stays at penis height with her mouth on the viewer's penis, shoulders and hands close to his pelvis in the lower foreground"
|
||||||
"semen",
|
)
|
||||||
"cumshot",
|
return oral_sentence(
|
||||||
"climax",
|
"POV side-lying cunnilingus position: the woman lies on her side with her top thigh lifted while the viewer lies beside her hips; "
|
||||||
)
|
"his face is at pussy height, with her thigh, hip, and torso forming a clear side-profile first-person frame"
|
||||||
if not any(token in context or token in action_lower for token in penetrative_tokens):
|
)
|
||||||
|
if (
|
||||||
|
"edge-supported oral" in position_context
|
||||||
|
or "edge-of-bed oral" in position_context
|
||||||
|
or "edge of bed oral" in position_context
|
||||||
|
or "raised edge" in position_context
|
||||||
|
):
|
||||||
|
if woman_gives and not man_gives:
|
||||||
|
return oral_sentence(
|
||||||
|
"POV raised-edge oral position: the viewer sits at the raised edge with legs apart while the woman kneels directly between his thighs; "
|
||||||
|
"her head is at penis height, mouth on the viewer's penis, with his thighs framing her shoulders in the lower foreground"
|
||||||
|
)
|
||||||
|
return oral_sentence(
|
||||||
|
"POV raised-edge cunnilingus position: the woman reclines at the raised edge with thighs open toward the viewer; "
|
||||||
|
"the viewer kneels between her legs with his face at pussy height, her hips and open thighs framing the first-person view"
|
||||||
|
)
|
||||||
|
if "chair oral" in position_context:
|
||||||
|
if woman_gives and not man_gives:
|
||||||
|
return oral_sentence(
|
||||||
|
"POV chair oral position: the viewer sits in a chair with legs apart while the woman kneels between the viewer's thighs; "
|
||||||
|
"her head is low at his pelvis, mouth on the viewer's penis, with chair seat, thighs, and hands anchoring the lower foreground"
|
||||||
|
)
|
||||||
|
return oral_sentence(
|
||||||
|
"POV chair cunnilingus position: the woman sits in the chair with thighs open while the viewer kneels between her legs; "
|
||||||
|
"his face is at pussy height and her hips, knees, and chair seat define the first-person geometry"
|
||||||
|
)
|
||||||
|
if "standing oral" in position_context:
|
||||||
|
if man_gives and not woman_gives:
|
||||||
|
return oral_sentence(
|
||||||
|
"POV standing cunnilingus position: the woman stands braced with one thigh lifted while the viewer kneels in front of her; "
|
||||||
|
"his face is at pussy height, with her raised thigh, hips, and standing leg clearly framing the view"
|
||||||
|
)
|
||||||
|
return oral_sentence(
|
||||||
|
"POV standing oral position: the viewer stands over her with hips forward while the woman kneels directly in front of him at hip height; "
|
||||||
|
"her head is tilted up at penis level, mouth on the viewer's penis, with his thighs and hands in the lower foreground"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
"reclining cunnilingus" in position_context
|
||||||
|
or "spread-leg oral" in position_context
|
||||||
|
or "open-thigh" in position_context
|
||||||
|
or "open thigh" in position_context
|
||||||
|
):
|
||||||
|
if woman_gives and not man_gives:
|
||||||
|
return oral_sentence(
|
||||||
|
"POV reclining oral position: the viewer reclines with thighs apart while the woman kneels low between his legs; "
|
||||||
|
"her face stays at penis height with her mouth on the viewer's penis and his thighs framing the first-person view"
|
||||||
|
)
|
||||||
|
return oral_sentence(
|
||||||
|
"POV open-thigh cunnilingus position: the woman reclines on her back with thighs spread toward the viewer; "
|
||||||
|
"the viewer kneels between her legs with his face at pussy height, her knees, hips, and torso aligned toward the camera"
|
||||||
|
)
|
||||||
|
if "straddled oral" in position_context:
|
||||||
|
if woman_gives and not man_gives:
|
||||||
|
return oral_sentence(
|
||||||
|
"POV straddled oral position: the viewer leans forward near the woman's face while she kneels below his pelvis; "
|
||||||
|
"her mouth stays on the viewer's penis with her head tilted upward and his thighs framing the lower foreground"
|
||||||
|
)
|
||||||
|
return oral_sentence(
|
||||||
|
"POV straddled cunnilingus position: the woman straddles above the viewer's face with her thighs framing his head; "
|
||||||
|
"her pussy stays directly over the viewer's mouth in a close first-person oral frame"
|
||||||
|
)
|
||||||
|
if "kneeling oral" in position_context or "kneeling" in position_context:
|
||||||
|
if man_gives and not woman_gives:
|
||||||
|
return oral_sentence(
|
||||||
|
"POV kneeling cunnilingus position: the woman kneels with thighs parted and hips angled forward while the viewer kneels in front of her; "
|
||||||
|
"his face is at pussy height, with her knees, hips, and torso readable from the first-person angle"
|
||||||
|
)
|
||||||
|
return oral_sentence(
|
||||||
|
"POV kneeling oral position: the viewer stands over her with hips forward while the woman kneels directly in front of him; "
|
||||||
|
"her head is at penis height, mouth on the viewer's penis, shoulders below his hips and his thighs framing the lower foreground"
|
||||||
|
)
|
||||||
|
if man_gives and not woman_gives:
|
||||||
|
return oral_sentence(
|
||||||
|
"POV cunnilingus position: the woman lies back with thighs open toward the viewer while he kneels between her legs; "
|
||||||
|
"his face is at pussy height, with her knees, hips, and torso forming the first-person frame"
|
||||||
|
)
|
||||||
|
return oral_sentence(
|
||||||
|
"POV oral position: the woman kneels close at the viewer's pelvis with her head at penis height; "
|
||||||
|
"her mouth is on the viewer's penis, shoulders between his thighs, and the viewer's hands or thighs anchor the lower foreground"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_penetrative_context:
|
||||||
return ""
|
return ""
|
||||||
oral_only = any(token in context for token in ("oral", "blowjob", "cunnilingus", "mouth on", "penis in her mouth"))
|
oral_only = any(token in context for token in ("oral", "blowjob", "cunnilingus", "mouth on", "penis in her mouth"))
|
||||||
if oral_only and not any(token in context for token in ("penetrat", "thrust", "anal", "ejaculat", "semen", "cumshot", "climax")):
|
if oral_only and not any(token in context for token in ("penetrat", "thrust", "anal", "ejaculat", "semen", "cumshot", "climax")):
|
||||||
|
|||||||
+144
-8
@@ -69,6 +69,58 @@ THEMATIC_LOCATION_PRESETS = {
|
|||||||
"partly hidden frame behind carved columns and shelf edges",
|
"partly hidden frame behind carved columns and shelf edges",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"creator_bedroom": {
|
||||||
|
"locations": [
|
||||||
|
{"slug": "creator_bedroom_ring_light", "prompt": "private creator bedroom with a ring light, phone tripod, rumpled bedding, and warm lamps"},
|
||||||
|
{"slug": "hotel_bed_phone_tripod", "prompt": "hotel bed content setup with a phone on a mini tripod, city lights, and satin bedding"},
|
||||||
|
{"slug": "studio_bedroom_backdrop", "prompt": "small creator studio with a bed, seamless backdrop, ring light, and visible phone stand"},
|
||||||
|
],
|
||||||
|
"compositions": [
|
||||||
|
"creator bedroom frame with bed edge and phone tripod readable",
|
||||||
|
"vertical creator-shot frame with ring light and warm lamps behind the body",
|
||||||
|
"bedside content setup composition with bedding and tripod placement visible",
|
||||||
|
"close room-context frame keeping the phone setup and bed plane clear",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"mirror_room": {
|
||||||
|
"locations": [
|
||||||
|
{"slug": "large_bedroom_mirror_selfie", "prompt": "large bedroom mirror with the phone visible, bed behind the subject, and warm side lamps"},
|
||||||
|
{"slug": "neon_mirror_wall", "prompt": "neon mirror wall with glossy floor reflections and saturated magenta-blue edge light"},
|
||||||
|
{"slug": "gold_vanity_mirror", "prompt": "gold-framed vanity mirror with makeup lights, silk fabric, and close reflected framing"},
|
||||||
|
],
|
||||||
|
"compositions": [
|
||||||
|
"mirror-room frame with the reflected phone angle and room depth aligned",
|
||||||
|
"full-length mirror composition keeping reflection lines readable",
|
||||||
|
"vanity-mirror frame with bulbs and reflected body plane visible",
|
||||||
|
"glossy mirror-wall composition with floor reflection line at the lower edge",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"boudoir_bedroom": {
|
||||||
|
"locations": [
|
||||||
|
{"slug": "warm_boudoir_canopy_bed", "prompt": "warm boudoir bedroom with satin sheets, canopy curtains, low lamplight, and bedside phone framing"},
|
||||||
|
{"slug": "velvet_headboard_bedroom", "prompt": "velvet headboard bedroom with gold lamps, rumpled bedding, and close sensual framing"},
|
||||||
|
{"slug": "hotel_satin_bedroom", "prompt": "luxury hotel bedroom with satin bedding, city glow, and a visible mirror near the bed"},
|
||||||
|
],
|
||||||
|
"compositions": [
|
||||||
|
"boudoir bedroom frame with sheet folds and warm bedroom depth visible",
|
||||||
|
"bed-edge composition with pillows, lamp glow, and headboard depth",
|
||||||
|
"low bedroom frame using bedding lines as the foreground anchor",
|
||||||
|
"hotel-bed composition with satin sheets and mirror edge readable",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"fetish_studio": {
|
||||||
|
"locations": [
|
||||||
|
{"slug": "black_latex_studio_floor", "prompt": "dark private studio with glossy black floor reflections, rim light, and a phone tripod"},
|
||||||
|
{"slug": "red_velvet_lacquer_room", "prompt": "red velvet room with black lacquer furniture, low spotlights, and reflective surfaces"},
|
||||||
|
{"slug": "chrome_fetish_set", "prompt": "chrome studio set with reflective panels, black curtains, and hard-edged erotic lighting"},
|
||||||
|
],
|
||||||
|
"compositions": [
|
||||||
|
"private studio frame with glossy floor reflection and controlled rim light",
|
||||||
|
"lacquer-room composition with reflective furniture and backdrop depth",
|
||||||
|
"chrome studio frame with panel seams and lighting stands readable",
|
||||||
|
"low studio-floor composition keeping reflection lines and set geometry clear",
|
||||||
|
],
|
||||||
|
},
|
||||||
"semi_public_affair": {
|
"semi_public_affair": {
|
||||||
"locations": [
|
"locations": [
|
||||||
{"slug": "hotel_corridor_affair", "prompt": "upscale hotel corridor with repeating numbered doors, patterned carpet, brass wall lamps, luggage carts, and a secluded corner near a service alcove"},
|
{"slug": "hotel_corridor_affair", "prompt": "upscale hotel corridor with repeating numbered doors, patterned carpet, brass wall lamps, luggage carts, and a secluded corner near a service alcove"},
|
||||||
@@ -268,12 +320,57 @@ def location_pool_names_for_preset(preset: str) -> list[str]:
|
|||||||
return names
|
return names
|
||||||
|
|
||||||
|
|
||||||
def custom_location_entries(custom_locations: str) -> list[dict[str, str]]:
|
def entry_prompt_text(value: Any) -> str:
|
||||||
entries: list[dict[str, str]] = []
|
if isinstance(value, dict):
|
||||||
|
return str(
|
||||||
|
value.get("prompt")
|
||||||
|
or value.get("template")
|
||||||
|
or value.get("text")
|
||||||
|
or value.get("description")
|
||||||
|
or value.get("name")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
return str(value or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def json_line_entries(line: str, field_name: str) -> list[Any] | None:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line[0] not in "[{":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
parsed = json.loads(line)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"Invalid JSON line in {field_name}: {exc}") from exc
|
||||||
|
if isinstance(parsed, list):
|
||||||
|
return parsed
|
||||||
|
return [parsed]
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_custom_location_entry(value: Any) -> dict[str, Any]:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
entry = dict(value)
|
||||||
|
prompt = entry_prompt_text(entry)
|
||||||
|
if not prompt:
|
||||||
|
raise ValueError(f"Custom location JSON entry is missing prompt/text/description/name: {value!r}")
|
||||||
|
entry["slug"] = _slug(str(entry.get("slug") or entry.get("name") or prompt))
|
||||||
|
entry["prompt"] = prompt
|
||||||
|
return entry
|
||||||
|
prompt = str(value or "").strip()
|
||||||
|
if not prompt:
|
||||||
|
raise ValueError("Custom location entry cannot be empty")
|
||||||
|
return {"slug": _slug(prompt), "prompt": prompt}
|
||||||
|
|
||||||
|
|
||||||
|
def custom_location_entries(custom_locations: str) -> list[dict[str, Any]]:
|
||||||
|
entries: list[dict[str, Any]] = []
|
||||||
for raw_line in str(custom_locations or "").splitlines():
|
for raw_line in str(custom_locations or "").splitlines():
|
||||||
line = raw_line.strip()
|
line = raw_line.strip()
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
|
json_entries = json_line_entries(line, "custom_locations")
|
||||||
|
if json_entries is not None:
|
||||||
|
entries.extend(normalize_custom_location_entry(entry) for entry in json_entries)
|
||||||
|
continue
|
||||||
slug = ""
|
slug = ""
|
||||||
prompt = line
|
prompt = line
|
||||||
if ":" in line:
|
if ":" in line:
|
||||||
@@ -322,6 +419,7 @@ def build_location_pool_json(
|
|||||||
merged_entries = entries
|
merged_entries = entries
|
||||||
|
|
||||||
active = bool(enabled) and bool(merged_entries)
|
active = bool(enabled) and bool(merged_entries)
|
||||||
|
theme = str(incoming.get("theme") or "") if combine_mode == "add" and incoming.get("enabled") else ""
|
||||||
summary = (
|
summary = (
|
||||||
f"{apply_mode}; pools={len(merged_pool_names)}; locations={len(merged_entries)}"
|
f"{apply_mode}; pools={len(merged_pool_names)}; locations={len(merged_entries)}"
|
||||||
if active
|
if active
|
||||||
@@ -334,6 +432,7 @@ def build_location_pool_json(
|
|||||||
"pool_names": merged_pool_names,
|
"pool_names": merged_pool_names,
|
||||||
"scene_entries": merged_entries,
|
"scene_entries": merged_entries,
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
|
"theme": theme,
|
||||||
},
|
},
|
||||||
ensure_ascii=True,
|
ensure_ascii=True,
|
||||||
sort_keys=True,
|
sort_keys=True,
|
||||||
@@ -342,7 +441,7 @@ def build_location_pool_json(
|
|||||||
|
|
||||||
def parse_location_config(location_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
def parse_location_config(location_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||||
if not location_config:
|
if not location_config:
|
||||||
return {"enabled": False, "apply_mode": "replace", "pool_names": [], "scene_entries": []}
|
return {"enabled": False, "apply_mode": "replace", "pool_names": [], "scene_entries": [], "theme": ""}
|
||||||
if isinstance(location_config, dict):
|
if isinstance(location_config, dict):
|
||||||
raw = dict(location_config)
|
raw = dict(location_config)
|
||||||
else:
|
else:
|
||||||
@@ -361,6 +460,7 @@ def parse_location_config(location_config: str | dict[str, Any] | None) -> dict[
|
|||||||
"pool_names": [str(name) for name in _list_from(raw.get("pool_names")) if str(name).strip()],
|
"pool_names": [str(name) for name in _list_from(raw.get("pool_names")) if str(name).strip()],
|
||||||
"scene_entries": entries,
|
"scene_entries": entries,
|
||||||
"summary": str(raw.get("summary") or ""),
|
"summary": str(raw.get("summary") or ""),
|
||||||
|
"theme": str(raw.get("theme") or ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -386,12 +486,30 @@ def composition_pool_names_for_preset(preset: str) -> list[str]:
|
|||||||
return names
|
return names
|
||||||
|
|
||||||
|
|
||||||
def custom_composition_entries(custom_compositions: str) -> list[str]:
|
def normalize_custom_composition_entry(value: Any) -> Any:
|
||||||
entries: list[str] = []
|
if isinstance(value, dict):
|
||||||
|
entry = dict(value)
|
||||||
|
prompt = entry_prompt_text(entry)
|
||||||
|
if not prompt:
|
||||||
|
raise ValueError(f"Custom composition JSON entry is missing prompt/text/description/name: {value!r}")
|
||||||
|
entry["prompt"] = prompt
|
||||||
|
return entry
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
raise ValueError("Custom composition entry cannot be empty")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def custom_composition_entries(custom_compositions: str) -> list[Any]:
|
||||||
|
entries: list[Any] = []
|
||||||
for raw_line in str(custom_compositions or "").splitlines():
|
for raw_line in str(custom_compositions or "").splitlines():
|
||||||
line = raw_line.strip()
|
line = raw_line.strip()
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
|
json_entries = json_line_entries(line, "custom_compositions")
|
||||||
|
if json_entries is not None:
|
||||||
|
entries.extend(normalize_custom_composition_entry(entry) for entry in json_entries)
|
||||||
|
continue
|
||||||
entries.append(line)
|
entries.append(line)
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
@@ -432,6 +550,7 @@ def build_composition_pool_json(
|
|||||||
merged_entries = entries
|
merged_entries = entries
|
||||||
|
|
||||||
active = bool(enabled) and bool(merged_entries)
|
active = bool(enabled) and bool(merged_entries)
|
||||||
|
theme = str(incoming.get("theme") or "") if combine_mode == "add" and incoming.get("enabled") else ""
|
||||||
summary = (
|
summary = (
|
||||||
f"{apply_mode}; pools={len(merged_pool_names)}; compositions={len(merged_entries)}"
|
f"{apply_mode}; pools={len(merged_pool_names)}; compositions={len(merged_entries)}"
|
||||||
if active
|
if active
|
||||||
@@ -444,6 +563,7 @@ def build_composition_pool_json(
|
|||||||
"pool_names": merged_pool_names,
|
"pool_names": merged_pool_names,
|
||||||
"composition_entries": merged_entries,
|
"composition_entries": merged_entries,
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
|
"theme": theme,
|
||||||
},
|
},
|
||||||
ensure_ascii=True,
|
ensure_ascii=True,
|
||||||
sort_keys=True,
|
sort_keys=True,
|
||||||
@@ -452,7 +572,7 @@ def build_composition_pool_json(
|
|||||||
|
|
||||||
def parse_composition_config(composition_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
def parse_composition_config(composition_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||||
if not composition_config:
|
if not composition_config:
|
||||||
return {"enabled": False, "apply_mode": "replace", "pool_names": [], "composition_entries": []}
|
return {"enabled": False, "apply_mode": "replace", "pool_names": [], "composition_entries": [], "theme": ""}
|
||||||
if isinstance(composition_config, dict):
|
if isinstance(composition_config, dict):
|
||||||
raw = dict(composition_config)
|
raw = dict(composition_config)
|
||||||
else:
|
else:
|
||||||
@@ -471,6 +591,7 @@ def parse_composition_config(composition_config: str | dict[str, Any] | None) ->
|
|||||||
"pool_names": [str(name) for name in _list_from(raw.get("pool_names")) if str(name).strip()],
|
"pool_names": [str(name) for name in _list_from(raw.get("pool_names")) if str(name).strip()],
|
||||||
"composition_entries": entries,
|
"composition_entries": entries,
|
||||||
"summary": str(raw.get("summary") or ""),
|
"summary": str(raw.get("summary") or ""),
|
||||||
|
"theme": str(raw.get("theme") or ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -512,8 +633,23 @@ def build_thematic_location_json(
|
|||||||
custom_compositions=composition_lines,
|
custom_compositions=composition_lines,
|
||||||
composition_config=composition_config or "",
|
composition_config=composition_config or "",
|
||||||
)
|
)
|
||||||
location_summary = json.loads(resolved_location_config).get("summary", "")
|
location_payload = json.loads(resolved_location_config)
|
||||||
composition_summary = json.loads(resolved_composition_config).get("summary", "")
|
composition_payload = json.loads(resolved_composition_config)
|
||||||
|
location_payload["theme"] = str(theme or "")
|
||||||
|
composition_payload["theme"] = str(theme or "")
|
||||||
|
themed_scene_entries = []
|
||||||
|
for entry in location_payload.get("scene_entries") or []:
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
themed_entry = dict(entry)
|
||||||
|
themed_entry.setdefault("theme", str(theme or ""))
|
||||||
|
themed_scene_entries.append(themed_entry)
|
||||||
|
else:
|
||||||
|
themed_scene_entries.append(entry)
|
||||||
|
location_payload["scene_entries"] = themed_scene_entries
|
||||||
|
resolved_location_config = json.dumps(location_payload, ensure_ascii=True, sort_keys=True)
|
||||||
|
resolved_composition_config = json.dumps(composition_payload, ensure_ascii=True, sort_keys=True)
|
||||||
|
location_summary = location_payload.get("summary", "")
|
||||||
|
composition_summary = composition_payload.get("summary", "")
|
||||||
summary = f"{theme}; locations={location_summary}; compositions={composition_summary}"
|
summary = f"{theme}; locations={location_summary}; compositions={composition_summary}"
|
||||||
return resolved_location_config, resolved_composition_config, summary
|
return resolved_location_config, resolved_composition_config, summary
|
||||||
|
|
||||||
|
|||||||
+39
-21
@@ -1,8 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .caption_naturalizer import naturalize_caption
|
from .caption_naturalizer import naturalize_caption_with_trace
|
||||||
from .caption_policy import caption_profile_choices
|
from .caption_policy import caption_profile_choices, style_policy_choices
|
||||||
|
from .formatter_detail import detail_level_choices
|
||||||
|
from .formatter_input import INPUT_HINT_CAPTION_OR_PROMPT, INPUT_HINT_PROMPT, input_hint_choices
|
||||||
|
from .formatter_target import target_choices
|
||||||
|
from .krea_format_route import style_mode_choices
|
||||||
from .krea_formatter import format_krea2_prompt
|
from .krea_formatter import format_krea2_prompt
|
||||||
from .sdxl_formatter import (
|
from .sdxl_formatter import (
|
||||||
format_sdxl_prompt,
|
format_sdxl_prompt,
|
||||||
@@ -11,8 +15,12 @@ try:
|
|||||||
sdxl_style_preset_choices,
|
sdxl_style_preset_choices,
|
||||||
)
|
)
|
||||||
except ImportError: # Allows local smoke tests from the repository root.
|
except ImportError: # Allows local smoke tests from the repository root.
|
||||||
from caption_naturalizer import naturalize_caption
|
from caption_naturalizer import naturalize_caption_with_trace
|
||||||
from caption_policy import caption_profile_choices
|
from caption_policy import caption_profile_choices, style_policy_choices
|
||||||
|
from formatter_detail import detail_level_choices
|
||||||
|
from formatter_input import INPUT_HINT_CAPTION_OR_PROMPT, INPUT_HINT_PROMPT, input_hint_choices
|
||||||
|
from formatter_target import target_choices
|
||||||
|
from krea_format_route import style_mode_choices
|
||||||
from krea_formatter import format_krea2_prompt
|
from krea_formatter import format_krea2_prompt
|
||||||
from sdxl_formatter import (
|
from sdxl_formatter import (
|
||||||
format_sdxl_prompt,
|
format_sdxl_prompt,
|
||||||
@@ -28,12 +36,13 @@ class SxCPCaptionNaturalizer:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"source_text": ("STRING", {"default": "", "multiline": True}),
|
"source_text": ("STRING", {"default": "", "multiline": True}),
|
||||||
"input_hint": (["auto", "metadata_json", "caption_or_prompt"], {"default": "auto"}),
|
"input_hint": (input_hint_choices(text_hint=INPUT_HINT_CAPTION_OR_PROMPT), {"default": "auto"}),
|
||||||
"caption_profile": (caption_profile_choices(), {"default": "manual_controls"}),
|
"caption_profile": (caption_profile_choices(), {"default": "manual_controls"}),
|
||||||
"detail_level": (["balanced", "concise", "dense"], {"default": "balanced"}),
|
"detail_level": (detail_level_choices(), {"default": "balanced"}),
|
||||||
"style_policy": (["drop_style_tail", "keep_style_terms"], {"default": "drop_style_tail"}),
|
"style_policy": (style_policy_choices(), {"default": "drop_style_tail"}),
|
||||||
"trigger": ("STRING", {"default": "sxcppnl7"}),
|
"trigger": ("STRING", {"default": "sxcppnl7"}),
|
||||||
"include_trigger": ("BOOLEAN", {"default": True}),
|
"include_trigger": ("BOOLEAN", {"default": True}),
|
||||||
|
"target": (target_choices(), {"default": "auto"}),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"source_text_input": ("STRING", {"forceInput": True}),
|
"source_text_input": ("STRING", {"forceInput": True}),
|
||||||
@@ -41,8 +50,8 @@ class SxCPCaptionNaturalizer:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("STRING", "STRING")
|
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
||||||
RETURN_NAMES = ("natural_caption", "method")
|
RETURN_NAMES = ("natural_caption", "method", "route_trace_json")
|
||||||
FUNCTION = "build"
|
FUNCTION = "build"
|
||||||
CATEGORY = "prompt_builder"
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
@@ -55,14 +64,16 @@ class SxCPCaptionNaturalizer:
|
|||||||
style_policy,
|
style_policy,
|
||||||
trigger,
|
trigger,
|
||||||
include_trigger,
|
include_trigger,
|
||||||
|
target="auto",
|
||||||
source_text_input="",
|
source_text_input="",
|
||||||
metadata_json="",
|
metadata_json="",
|
||||||
):
|
):
|
||||||
active_source_text = source_text_input or source_text or ""
|
active_source_text = source_text_input or source_text or ""
|
||||||
return naturalize_caption(
|
return naturalize_caption_with_trace(
|
||||||
source_text=active_source_text,
|
source_text=active_source_text,
|
||||||
metadata_json=metadata_json or "",
|
metadata_json=metadata_json or "",
|
||||||
input_hint=input_hint,
|
input_hint=input_hint,
|
||||||
|
target=target,
|
||||||
trigger=trigger,
|
trigger=trigger,
|
||||||
include_trigger=include_trigger,
|
include_trigger=include_trigger,
|
||||||
detail_level=detail_level,
|
detail_level=detail_level,
|
||||||
@@ -77,21 +88,22 @@ class SxCPKrea2Formatter:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"source_text": ("STRING", {"default": "", "multiline": True}),
|
"source_text": ("STRING", {"default": "", "multiline": True}),
|
||||||
"input_hint": (["auto", "metadata_json", "prompt"], {"default": "auto"}),
|
"input_hint": (input_hint_choices(text_hint=INPUT_HINT_PROMPT), {"default": "auto"}),
|
||||||
"target": (["auto", "single", "softcore", "hardcore"], {"default": "auto"}),
|
"target": (target_choices(), {"default": "auto"}),
|
||||||
"detail_level": (["balanced", "concise", "dense"], {"default": "balanced"}),
|
"detail_level": (detail_level_choices(), {"default": "balanced"}),
|
||||||
"style_mode": (["preserve", "photographic", "minimal"], {"default": "preserve"}),
|
"style_mode": (style_mode_choices(), {"default": "preserve"}),
|
||||||
"preserve_trigger": ("BOOLEAN", {"default": False}),
|
"preserve_trigger": ("BOOLEAN", {"default": False}),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"metadata_json": ("STRING", {"default": "", "multiline": True}),
|
"source_text_input": ("STRING", {"forceInput": True}),
|
||||||
"negative_prompt": ("STRING", {"default": "", "multiline": True}),
|
"metadata_json": ("STRING", {"forceInput": True}),
|
||||||
|
"negative_prompt": ("STRING", {"forceInput": True}),
|
||||||
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
||||||
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING")
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING")
|
||||||
RETURN_NAMES = (
|
RETURN_NAMES = (
|
||||||
"krea_prompt",
|
"krea_prompt",
|
||||||
"negative_prompt",
|
"negative_prompt",
|
||||||
@@ -100,6 +112,7 @@ class SxCPKrea2Formatter:
|
|||||||
"softcore_negative_prompt",
|
"softcore_negative_prompt",
|
||||||
"hardcore_negative_prompt",
|
"hardcore_negative_prompt",
|
||||||
"method",
|
"method",
|
||||||
|
"route_trace_json",
|
||||||
)
|
)
|
||||||
FUNCTION = "build"
|
FUNCTION = "build"
|
||||||
CATEGORY = "prompt_builder"
|
CATEGORY = "prompt_builder"
|
||||||
@@ -112,13 +125,15 @@ class SxCPKrea2Formatter:
|
|||||||
detail_level,
|
detail_level,
|
||||||
style_mode,
|
style_mode,
|
||||||
preserve_trigger,
|
preserve_trigger,
|
||||||
|
source_text_input="",
|
||||||
metadata_json="",
|
metadata_json="",
|
||||||
negative_prompt="",
|
negative_prompt="",
|
||||||
extra_positive="",
|
extra_positive="",
|
||||||
extra_negative="",
|
extra_negative="",
|
||||||
):
|
):
|
||||||
|
active_source_text = source_text_input or source_text or ""
|
||||||
row = format_krea2_prompt(
|
row = format_krea2_prompt(
|
||||||
source_text=source_text or "",
|
source_text=active_source_text,
|
||||||
metadata_json=metadata_json or "",
|
metadata_json=metadata_json or "",
|
||||||
negative_prompt=negative_prompt or "",
|
negative_prompt=negative_prompt or "",
|
||||||
input_hint=input_hint,
|
input_hint=input_hint,
|
||||||
@@ -137,6 +152,7 @@ class SxCPKrea2Formatter:
|
|||||||
row["softcore_negative_prompt"],
|
row["softcore_negative_prompt"],
|
||||||
row["hardcore_negative_prompt"],
|
row["hardcore_negative_prompt"],
|
||||||
row["method"],
|
row["method"],
|
||||||
|
row["route_trace_json"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -146,8 +162,8 @@ class SxCPSDXLFormatter:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"source_text": ("STRING", {"default": "", "multiline": True}),
|
"source_text": ("STRING", {"default": "", "multiline": True}),
|
||||||
"input_hint": (["auto", "metadata_json", "prompt"], {"default": "auto"}),
|
"input_hint": (input_hint_choices(text_hint=INPUT_HINT_PROMPT), {"default": "auto"}),
|
||||||
"target": (["auto", "single", "softcore", "hardcore"], {"default": "auto"}),
|
"target": (target_choices(), {"default": "auto"}),
|
||||||
"formatter_profile": (sdxl_formatter_profile_choices(), {"default": "manual_controls"}),
|
"formatter_profile": (sdxl_formatter_profile_choices(), {"default": "manual_controls"}),
|
||||||
"style_preset": (sdxl_style_preset_choices(), {"default": "flat_vector_pony"}),
|
"style_preset": (sdxl_style_preset_choices(), {"default": "flat_vector_pony"}),
|
||||||
"quality_preset": (sdxl_quality_preset_choices(), {"default": "pony_high"}),
|
"quality_preset": (sdxl_quality_preset_choices(), {"default": "pony_high"}),
|
||||||
@@ -167,7 +183,7 @@ class SxCPSDXLFormatter:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING")
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING")
|
||||||
RETURN_NAMES = (
|
RETURN_NAMES = (
|
||||||
"sdxl_prompt",
|
"sdxl_prompt",
|
||||||
"negative_prompt",
|
"negative_prompt",
|
||||||
@@ -176,6 +192,7 @@ class SxCPSDXLFormatter:
|
|||||||
"softcore_negative_prompt",
|
"softcore_negative_prompt",
|
||||||
"hardcore_negative_prompt",
|
"hardcore_negative_prompt",
|
||||||
"method",
|
"method",
|
||||||
|
"route_trace_json",
|
||||||
)
|
)
|
||||||
FUNCTION = "build"
|
FUNCTION = "build"
|
||||||
CATEGORY = "prompt_builder"
|
CATEGORY = "prompt_builder"
|
||||||
@@ -227,6 +244,7 @@ class SxCPSDXLFormatter:
|
|||||||
row["softcore_negative_prompt"],
|
row["softcore_negative_prompt"],
|
||||||
row["hardcore_negative_prompt"],
|
row["hardcore_negative_prompt"],
|
||||||
row["method"],
|
row["method"],
|
||||||
|
row["route_trace_json"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+7
-19
@@ -13,6 +13,7 @@ try:
|
|||||||
from .prompt_builder import (
|
from .prompt_builder import (
|
||||||
subcategory_choices,
|
subcategory_choices,
|
||||||
)
|
)
|
||||||
|
from .seed_config import configured_seed_from_axes
|
||||||
from .location_config import (
|
from .location_config import (
|
||||||
build_composition_pool_json,
|
build_composition_pool_json,
|
||||||
build_location_pool_json,
|
build_location_pool_json,
|
||||||
@@ -31,6 +32,7 @@ except ImportError: # Allows local smoke tests from the repository root.
|
|||||||
from prompt_builder import (
|
from prompt_builder import (
|
||||||
subcategory_choices,
|
subcategory_choices,
|
||||||
)
|
)
|
||||||
|
from seed_config import configured_seed_from_axes
|
||||||
from location_config import (
|
from location_config import (
|
||||||
build_composition_pool_json,
|
build_composition_pool_json,
|
||||||
build_location_pool_json,
|
build_location_pool_json,
|
||||||
@@ -224,25 +226,11 @@ class SxCPCastBias:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _configured_cast_seed(seed_config):
|
def _configured_cast_seed(seed_config):
|
||||||
if not seed_config:
|
return configured_seed_from_axes(
|
||||||
return None
|
seed_config,
|
||||||
if isinstance(seed_config, dict):
|
("category", "content", "role"),
|
||||||
raw = seed_config
|
extra_keys=("seed", "global_seed"),
|
||||||
else:
|
)
|
||||||
try:
|
|
||||||
raw = json.loads(str(seed_config))
|
|
||||||
except (TypeError, ValueError, json.JSONDecodeError):
|
|
||||||
return None
|
|
||||||
if not isinstance(raw, dict):
|
|
||||||
return None
|
|
||||||
for key in ("category_seed", "content_seed", "role_seed", "seed", "global_seed"):
|
|
||||||
try:
|
|
||||||
value = int(raw.get(key))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
continue
|
|
||||||
if value >= 0:
|
|
||||||
return value
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _weight_pairs(weights_text, start_count):
|
def _weight_pairs(weights_text, start_count):
|
||||||
|
|||||||
+1262
File diff suppressed because it is too large
Load Diff
+54
-57
@@ -8,12 +8,18 @@ try:
|
|||||||
from .seed_config import (
|
from .seed_config import (
|
||||||
build_seed_config_json,
|
build_seed_config_json,
|
||||||
build_seed_lock_config_json,
|
build_seed_lock_config_json,
|
||||||
|
configured_seed_from_axes,
|
||||||
|
normalize_reroll_axis,
|
||||||
|
seed_reroll_axis_choices,
|
||||||
seed_mode_choices,
|
seed_mode_choices,
|
||||||
)
|
)
|
||||||
except ImportError: # Allows local smoke tests from the repository root.
|
except ImportError: # Allows local smoke tests from the repository root.
|
||||||
from seed_config import (
|
from seed_config import (
|
||||||
build_seed_config_json,
|
build_seed_config_json,
|
||||||
build_seed_lock_config_json,
|
build_seed_lock_config_json,
|
||||||
|
configured_seed_from_axes,
|
||||||
|
normalize_reroll_axis,
|
||||||
|
seed_reroll_axis_choices,
|
||||||
seed_mode_choices,
|
seed_mode_choices,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,8 +82,8 @@ class SxCPSeedControl:
|
|||||||
required[f"{axis}_seed"] = ("INT", seed_spec)
|
required[f"{axis}_seed"] = ("INT", seed_spec)
|
||||||
return {"required": required}
|
return {"required": required}
|
||||||
|
|
||||||
RETURN_TYPES = (SXCP_SEED_CONFIG,)
|
RETURN_TYPES = (SXCP_SEED_CONFIG, "STRING")
|
||||||
RETURN_NAMES = ("seed_config",)
|
RETURN_NAMES = ("seed_config", "summary")
|
||||||
FUNCTION = "build"
|
FUNCTION = "build"
|
||||||
CATEGORY = "prompt_builder"
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
@@ -88,6 +94,21 @@ class SxCPSeedControl:
|
|||||||
return random.random()
|
return random.random()
|
||||||
return tuple(args), tuple(sorted(kwargs.items()))
|
return tuple(args), tuple(sorted(kwargs.items()))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _summary(cls, config_json):
|
||||||
|
try:
|
||||||
|
config = json.loads(config_json)
|
||||||
|
except (TypeError, ValueError, json.JSONDecodeError):
|
||||||
|
return "invalid seed config"
|
||||||
|
parts = []
|
||||||
|
for axis in cls.SEED_AXES:
|
||||||
|
try:
|
||||||
|
value = int(config.get(f"{axis}_seed", -1))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
value = -1
|
||||||
|
parts.append(f"{axis}={'follow_main' if value < 0 else value}")
|
||||||
|
return "resolved seeds: " + "; ".join(parts)
|
||||||
|
|
||||||
def build(
|
def build(
|
||||||
self,
|
self,
|
||||||
category_seed_mode,
|
category_seed_mode,
|
||||||
@@ -109,27 +130,29 @@ class SxCPSeedControl:
|
|||||||
composition_seed_mode,
|
composition_seed_mode,
|
||||||
composition_seed,
|
composition_seed,
|
||||||
):
|
):
|
||||||
|
config = build_seed_config_json(
|
||||||
|
category_seed=category_seed,
|
||||||
|
subcategory_seed=subcategory_seed,
|
||||||
|
content_seed=content_seed,
|
||||||
|
person_seed=person_seed,
|
||||||
|
scene_seed=scene_seed,
|
||||||
|
pose_seed=pose_seed,
|
||||||
|
role_seed=role_seed,
|
||||||
|
expression_seed=expression_seed,
|
||||||
|
composition_seed=composition_seed,
|
||||||
|
category_seed_mode=category_seed_mode,
|
||||||
|
subcategory_seed_mode=subcategory_seed_mode,
|
||||||
|
content_seed_mode=content_seed_mode,
|
||||||
|
person_seed_mode=person_seed_mode,
|
||||||
|
scene_seed_mode=scene_seed_mode,
|
||||||
|
pose_seed_mode=pose_seed_mode,
|
||||||
|
role_seed_mode=role_seed_mode,
|
||||||
|
expression_seed_mode=expression_seed_mode,
|
||||||
|
composition_seed_mode=composition_seed_mode,
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
build_seed_config_json(
|
config,
|
||||||
category_seed=category_seed,
|
self._summary(config),
|
||||||
subcategory_seed=subcategory_seed,
|
|
||||||
content_seed=content_seed,
|
|
||||||
person_seed=person_seed,
|
|
||||||
scene_seed=scene_seed,
|
|
||||||
pose_seed=pose_seed,
|
|
||||||
role_seed=role_seed,
|
|
||||||
expression_seed=expression_seed,
|
|
||||||
composition_seed=composition_seed,
|
|
||||||
category_seed_mode=category_seed_mode,
|
|
||||||
subcategory_seed_mode=subcategory_seed_mode,
|
|
||||||
content_seed_mode=content_seed_mode,
|
|
||||||
person_seed_mode=person_seed_mode,
|
|
||||||
scene_seed_mode=scene_seed_mode,
|
|
||||||
pose_seed_mode=pose_seed_mode,
|
|
||||||
role_seed_mode=role_seed_mode,
|
|
||||||
expression_seed_mode=expression_seed_mode,
|
|
||||||
composition_seed_mode=composition_seed_mode,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -163,20 +186,7 @@ class SxCPSeedLocker:
|
|||||||
"required": {
|
"required": {
|
||||||
"base_seed": ("INT", seed_spec),
|
"base_seed": ("INT", seed_spec),
|
||||||
"reroll_axis": (
|
"reroll_axis": (
|
||||||
[
|
seed_reroll_axis_choices(),
|
||||||
"none",
|
|
||||||
"category",
|
|
||||||
"subcategory",
|
|
||||||
"content",
|
|
||||||
"person",
|
|
||||||
"scene",
|
|
||||||
"pose",
|
|
||||||
"role",
|
|
||||||
"expression",
|
|
||||||
"composition",
|
|
||||||
"content_pose",
|
|
||||||
"scene_pose",
|
|
||||||
],
|
|
||||||
{"default": "none"},
|
{"default": "none"},
|
||||||
),
|
),
|
||||||
"reroll_seed": ("INT", reroll_seed_spec),
|
"reroll_seed": ("INT", reroll_seed_spec),
|
||||||
@@ -189,8 +199,9 @@ class SxCPSeedLocker:
|
|||||||
CATEGORY = "prompt_builder"
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
def build(self, base_seed, reroll_axis, reroll_seed):
|
def build(self, base_seed, reroll_axis, reroll_seed):
|
||||||
config = build_seed_lock_config_json(base_seed=base_seed, reroll_axis=reroll_axis, reroll_seed=reroll_seed)
|
normalized_axis = normalize_reroll_axis(reroll_axis)
|
||||||
summary = f"base {base_seed}; reroll {reroll_axis} with {'main seed' if int(reroll_seed) < 0 else reroll_seed}"
|
config = build_seed_lock_config_json(base_seed=base_seed, reroll_axis=normalized_axis, reroll_seed=reroll_seed)
|
||||||
|
summary = f"base {base_seed}; reroll {normalized_axis} with {'main seed' if int(reroll_seed) < 0 else reroll_seed}"
|
||||||
return config, summary
|
return config, summary
|
||||||
|
|
||||||
|
|
||||||
@@ -216,25 +227,11 @@ class SxCPSDXLBucketSize:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _configured_bucket_seed(seed_config):
|
def _configured_bucket_seed(seed_config):
|
||||||
if not seed_config:
|
return configured_seed_from_axes(
|
||||||
return None
|
seed_config,
|
||||||
if isinstance(seed_config, dict):
|
("composition", "content"),
|
||||||
raw = seed_config
|
extra_keys=("seed", "global_seed"),
|
||||||
else:
|
)
|
||||||
try:
|
|
||||||
raw = json.loads(str(seed_config))
|
|
||||||
except (TypeError, ValueError, json.JSONDecodeError):
|
|
||||||
return None
|
|
||||||
if not isinstance(raw, dict):
|
|
||||||
return None
|
|
||||||
for key in ("composition_seed", "content_seed", "seed", "global_seed"):
|
|
||||||
try:
|
|
||||||
value = int(raw.get(key))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
continue
|
|
||||||
if value >= 0:
|
|
||||||
return value
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def IS_CHANGED(cls, *args, **kwargs):
|
def IS_CHANGED(cls, *args, **kwargs):
|
||||||
|
|||||||
+103
-14
@@ -16,20 +16,60 @@ COMMON_INPUT_TOOLTIPS = {
|
|||||||
"generation_profile": "General style/intensity profile from SxCP Generation Profile.",
|
"generation_profile": "General style/intensity profile from SxCP Generation Profile.",
|
||||||
"filter_config": "Ethnicity/body filter config. Ethnicity List can feed this too.",
|
"filter_config": "Ethnicity/body filter config. Ethnicity List can feed this too.",
|
||||||
"ethnicity_list": "Optional ethnicity pool. When connected, it overrides the slot or generator ethnicity picker.",
|
"ethnicity_list": "Optional ethnicity pool. When connected, it overrides the slot or generator ethnicity picker.",
|
||||||
"seed_config": "Per-axis seed config. Connect Global Seed, Seed Locker, or Seed Control here.",
|
"seed_config": "Per-axis seed config. Use Global Seed for full reproducibility, Seed Locker to reroll one axis, or Seed Control for manual axis seeds.",
|
||||||
"camera_config": "Camera config used by the prompt formatter when camera mode is from_camera_config.",
|
"camera_config": "Camera config consumed only by nodes/options set to from_camera_config.",
|
||||||
"location_config": "Location config from SxCP Location Pool. It can replace or add to the category scene pool.",
|
"location_config": "Location config from SxCP Location Pool. It can replace or add to the category scene pool.",
|
||||||
"composition_config": "Composition config from SxCP Composition Pool or Location Theme. It can replace or add framing options.",
|
"composition_config": "Composition config from SxCP Composition Pool or Location Theme. It can replace or add framing options.",
|
||||||
"softcore_camera_config": "Camera config used only for the softcore Insta/OF prompt. Falls back to camera_config if empty.",
|
"softcore_camera_config": "Camera config used only for the softcore Insta/OF prompt. Falls back to camera_config if empty.",
|
||||||
"hardcore_camera_config": "Camera config used only for the hardcore Insta/OF prompt. Falls back to camera_config if empty.",
|
"hardcore_camera_config": "Camera config used only for the hardcore Insta/OF prompt. Falls back to camera_config if empty.",
|
||||||
"character_profile": "Saved or loaded single-character profile. Character slots override this for configured casts.",
|
"character_profile": "Saved or loaded single-character profile. Character slots override this for configured casts.",
|
||||||
"character_cast": "Chain character slots here. The node closest to the final generator becomes the next auto_chain label.",
|
"character_cast": "Chain slot output into the next slot, then into the generator. The first enabled woman/man in chain order becomes A, then B, and so on.",
|
||||||
"character_slot": "Single slot payload for saving/loading profiles or debugging one character.",
|
"character_slot": "Single slot payload for saving/loading profiles or debugging one character.",
|
||||||
"hardcore_position_config": "Hardcore action/position config. Chain Position Pool into Action Filter, then into the generator.",
|
"hardcore_position_config": "Hardcore action/position config. Chain Position Pool into Action Filter, then into the generator.",
|
||||||
"custom_locations": "One custom location per line. Use plain text, or slug: location text.",
|
"custom_locations": "One custom location per line. Use plain text, or slug: location text.",
|
||||||
"custom_compositions": "One custom composition/framing phrase per line.",
|
"custom_compositions": "One custom composition/framing phrase per line.",
|
||||||
"theme": "Matched location and composition theme, useful when the place needs compatible framing.",
|
"theme": "Matched location and composition theme, useful when the place needs compatible framing.",
|
||||||
"metadata_json": "Structured metadata from an SxCP generator. Prefer this over raw prompt text for formatters and profile save.",
|
"metadata_json": "Structured metadata from an SxCP generator. Prefer this over raw prompt text for formatters and profile save.",
|
||||||
|
"scene": "Structured v2 scene context. Chain Scene nodes in order, then connect to Scene Output or Scene Pair Output.",
|
||||||
|
"softcore_scene": "Softcore branch scene from Scene Branch Pair, optionally refined by Softcore Branch Options.",
|
||||||
|
"hardcore_scene": "Hardcore branch scene from Scene Branch Pair, optionally refined by Hardcore Branch Options.",
|
||||||
|
"target_formatter": "Intended downstream formatter target. The scene stores this as metadata; use formatter nodes for final rewriting.",
|
||||||
|
"category_preset": "Category preset this scene should render through when no explicit category config overrides it.",
|
||||||
|
"central_subject": "Who should be visually central in this scene metadata.",
|
||||||
|
"pov_participant": "Optional participant treated as the first-person viewer in later character/camera logic.",
|
||||||
|
"subject_label": "Character label affected by this layer. all applies the layer to every matching character slot.",
|
||||||
|
"wardrobe_prompt": "Optional wardrobe/set note carried as scene metadata and compatibility extra prompt text.",
|
||||||
|
"custom_location": "Exact location text for this scene. One line or JSON entry is enough.",
|
||||||
|
"location_note": "Additional location wording merged into the location pool entry.",
|
||||||
|
"foreground_anchors": "Objects or surfaces that should stay near the camera or lower frame.",
|
||||||
|
"repeated_background": "Repeating background structure such as desks, doors, shelves, pillars, or windows.",
|
||||||
|
"props": "Scene props or set dressing objects that make the location readable.",
|
||||||
|
"set_prompt": "Freeform set-dressing sentence appended to the scene layer.",
|
||||||
|
"blocking_mode": "Broad body-placement mode. custom lets custom_blocking carry the exact placement.",
|
||||||
|
"subject_placement": "Where the subject or cast sits in the space: foreground, near desk edge, on bed, in aisle, etc.",
|
||||||
|
"body_relation": "Spatial relationship between participants, separate from the action itself.",
|
||||||
|
"custom_blocking": "Exact blocking/positioning sentence for the scene layer.",
|
||||||
|
"scene_kind": "Regular, softcore, or hardcore intent for this action layer.",
|
||||||
|
"action_prompt": "Action text stored separately from blocking and camera. Use position pools for hardcore randomization when possible.",
|
||||||
|
"performance_prompt": "Expression, gaze, hand, and body-performance note stored separately from the action.",
|
||||||
|
"camera_prompt": "Optional freeform camera note kept as scene metadata. Camera config still controls existing formatter behavior.",
|
||||||
|
"custom_composition": "Exact composition/framing entry to add to the composition pool.",
|
||||||
|
"composition_prompt": "Additional composition wording merged into the composition layer.",
|
||||||
|
"lighting_source": "Main light source family for the scene.",
|
||||||
|
"lighting_softness": "Softness of the light: soft, balanced, or hard.",
|
||||||
|
"lighting_contrast": "Overall contrast level for the lighting layer.",
|
||||||
|
"color_temperature": "Warm, neutral, cool, or mixed color temperature.",
|
||||||
|
"custom_lighting": "Exact lighting sentence for the scene layer.",
|
||||||
|
"continuity": "How branch outputs share cast/location setup between softcore and hardcore scenes.",
|
||||||
|
"platform_style": "Instagram/OnlyFans styling bias for Scene Pair Output.",
|
||||||
|
"softcore_cast": "Whether the softcore branch uses a solo creator or the same cast as the hardcore branch.",
|
||||||
|
"hardcore_cast": "Hardcore branch cast preset or explicit count mode.",
|
||||||
|
"softcore_level": "Softcore exposure/style level for Scene Pair Output.",
|
||||||
|
"hardcore_level": "Hardcore intensity level for Scene Pair Output.",
|
||||||
|
"softcore_camera_mode": "Softcore branch camera mode, or from_camera_config to use the connected scene camera.",
|
||||||
|
"hardcore_camera_mode": "Hardcore branch camera mode, or from_camera_config to use the connected scene camera.",
|
||||||
|
"hardcore_clothing_continuity": "How wardrobe is rendered in the hardcore branch. explicit_nude avoids clothing-token conflicts.",
|
||||||
|
"hardcore_detail_density": "How much explicit action detail the current formatter route keeps for the hardcore branch.",
|
||||||
"source_text": "Raw prompt, caption, or metadata JSON depending on input_hint.",
|
"source_text": "Raw prompt, caption, or metadata JSON depending on input_hint.",
|
||||||
"source_text_input": "Optional linked raw prompt/caption input. When connected, it overrides the source_text widget.",
|
"source_text_input": "Optional linked raw prompt/caption input. When connected, it overrides the source_text widget.",
|
||||||
"input_hint": "Tells the node how to interpret source_text. auto tries metadata first.",
|
"input_hint": "Tells the node how to interpret source_text. auto tries metadata first.",
|
||||||
@@ -46,8 +86,8 @@ COMMON_INPUT_TOOLTIPS = {
|
|||||||
"megapixels": "Approximate megapixel count for the selected bucket.",
|
"megapixels": "Approximate megapixel count for the selected bucket.",
|
||||||
"enabled": "Enable this node's effect while keeping it wired in the graph.",
|
"enabled": "Enable this node's effect while keeping it wired in the graph.",
|
||||||
"combine_mode": "replace starts a new pool/config; add merges selected values into the incoming config.",
|
"combine_mode": "replace starts a new pool/config; add merges selected values into the incoming config.",
|
||||||
"manual": "Manual character details config. Non-empty manual fields override generated slot details.",
|
"manual": "Manual character details config from Manual Details. Non-empty fields override generated slot details.",
|
||||||
"characteristics": "Chainable character characteristic pool such as age/body/eyes/clothing.",
|
"characteristics": "Chainable character pool for controlled randomness such as age, body, eyes, and clothing.",
|
||||||
"hair_config": "Chainable hair pool. Combine length, color, and style nodes before the character slot.",
|
"hair_config": "Chainable hair pool. Combine length, color, and style nodes before the character slot.",
|
||||||
"summary": "Human-readable description of the config produced by this node.",
|
"summary": "Human-readable description of the config produced by this node.",
|
||||||
"status": "Operation result or warning text.",
|
"status": "Operation result or warning text.",
|
||||||
@@ -61,7 +101,7 @@ COMMON_INPUT_TOOLTIPS = {
|
|||||||
"source": "Where the save node reads character data from.",
|
"source": "Where the save node reads character data from.",
|
||||||
"subject_type": "Character type for this slot or saved profile.",
|
"subject_type": "Character type for this slot or saved profile.",
|
||||||
"label": "Character label. auto_chain assigns the next Woman/Man label based on incoming cast order.",
|
"label": "Character label. auto_chain assigns the next Woman/Man label based on incoming cast order.",
|
||||||
"slot_seed": "Per-character seed. Use -1 to follow the generator person seed.",
|
"slot_seed": "Per-character seed for age/body/hair/eyes random picks. Use -1 to derive from the generator person seed.",
|
||||||
"age": "Age choice for this slot. Use Age Range node for a custom random age pool.",
|
"age": "Age choice for this slot. Use Age Range node for a custom random age pool.",
|
||||||
"manual_age": "Exact age phrase override, for example '32-year-old adult'.",
|
"manual_age": "Exact age phrase override, for example '32-year-old adult'.",
|
||||||
"ethnicity": "Ethnicity choice for this slot. A connected Ethnicity List overrides this picker.",
|
"ethnicity": "Ethnicity choice for this slot. A connected Ethnicity List overrides this picker.",
|
||||||
@@ -83,9 +123,9 @@ COMMON_INPUT_TOOLTIPS = {
|
|||||||
"expression_intensity_mode": "For Generation Profile, choose profile_default, random, or fixed value from expression_intensity.",
|
"expression_intensity_mode": "For Generation Profile, choose profile_default, random, or fixed value from expression_intensity.",
|
||||||
"softcore_expression_intensity": "Optional expression intensity override for this character in softcore prompts. -1 inherits.",
|
"softcore_expression_intensity": "Optional expression intensity override for this character in softcore prompts. -1 inherits.",
|
||||||
"hardcore_expression_intensity": "Optional expression intensity override for this character in hardcore prompts. -1 inherits.",
|
"hardcore_expression_intensity": "Optional expression intensity override for this character in hardcore prompts. -1 inherits.",
|
||||||
"presence_mode": "Controls whether the character is visible, implied POV, or otherwise present.",
|
"presence_mode": "Controls whether the character is visible or acts as the male POV participant.",
|
||||||
"softcore_outfit": "Manual softcore outfit text for this character.",
|
"softcore_outfit": "Manual softcore outfit text for this character. Prefer Character Clothing for reusable outfit pools.",
|
||||||
"hardcore_clothing": "Manual hardcore clothing/body exposure text for this character.",
|
"hardcore_clothing": "Manual hardcore exposure text for this character. Use explicit nude states when you do not want clothing words repeated.",
|
||||||
"custom_softcore_outfits": "One custom softcore outfit per line. Used when softcore_source is custom.",
|
"custom_softcore_outfits": "One custom softcore outfit per line. Used when softcore_source is custom.",
|
||||||
"custom_hardcore_clothing": "One custom hardcore clothing/body exposure state per line.",
|
"custom_hardcore_clothing": "One custom hardcore clothing/body exposure state per line.",
|
||||||
"condition": "Loop condition. When false, the loop stops and passes current values through.",
|
"condition": "Loop condition. When false, the loop stops and passes current values through.",
|
||||||
@@ -93,8 +133,8 @@ COMMON_INPUT_TOOLTIPS = {
|
|||||||
"skip": "Number of leading loop indexes to skip. skip=1 starts generation at index 2.",
|
"skip": "Number of leading loop indexes to skip. skip=1 starts generation at index 2.",
|
||||||
"collection": "Existing accumulated value or batch.",
|
"collection": "Existing accumulated value or batch.",
|
||||||
"value": "Value to append, store, or pass through.",
|
"value": "Value to append, store, or pass through.",
|
||||||
"store_key": "Accumulator memory key. Same key shares stored entries across executions.",
|
"store_key": "Accumulator memory key. Leave blank for node-local storage, or use the same text to share one store across nodes.",
|
||||||
"store_key_input": "Connect SxCP Accumulator store_key here so preview/delete/save uses the same accumulator and graph dependency.",
|
"store_key_input": "Connect SxCP Accumulator store_key output here so preview/delete/save targets the same store and keeps a graph dependency.",
|
||||||
"action": "Accumulator operation: append, replace, clear, read, or append a variant.",
|
"action": "Accumulator operation: append, replace, clear, read, or append a variant.",
|
||||||
"max_items": "Maximum stored entries kept in this accumulator.",
|
"max_items": "Maximum stored entries kept in this accumulator.",
|
||||||
"image_batch_mode": "How image entries are batched when dimensions differ.",
|
"image_batch_mode": "How image entries are batched when dimensions differ.",
|
||||||
@@ -109,7 +149,7 @@ COMMON_INPUT_TOOLTIPS = {
|
|||||||
"delete_action": "Optional execution-time delete operation. JS buttons can delete interactively without setting this.",
|
"delete_action": "Optional execution-time delete operation. JS buttons can delete interactively without setting this.",
|
||||||
"delete_entry_id": "Entry id to delete when delete_action is delete_entry_id.",
|
"delete_entry_id": "Entry id to delete when delete_action is delete_entry_id.",
|
||||||
"delete_index": "1-based entry index to delete when delete_action is delete_index. 0 disables it.",
|
"delete_index": "1-based entry index to delete when delete_action is delete_index. 0 disables it.",
|
||||||
"save_batch": "When enabled, save all current accumulator images once finished is true.",
|
"save_batch": "When enabled, save all current accumulator images during node execution once finished is true.",
|
||||||
"finished": "Gate for saving. Outside a loop, leave true; inside a loop, wire a final-iteration signal.",
|
"finished": "Gate for saving. Outside a loop, leave true; inside a loop, wire a final-iteration signal.",
|
||||||
"save_path": "Folder to save the accumulator batch. Relative paths are inside ComfyUI output; absolute paths are used directly.",
|
"save_path": "Folder to save the accumulator batch. Relative paths are inside ComfyUI output; absolute paths are used directly.",
|
||||||
"filename_prefix": "Filename prefix for saved accumulator images.",
|
"filename_prefix": "Filename prefix for saved accumulator images.",
|
||||||
@@ -166,8 +206,11 @@ COMMON_INPUT_TOOLTIPS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NODE_INPUT_TOOLTIPS = {
|
NODE_INPUT_TOOLTIPS = {
|
||||||
|
"SxCPGlobalSeed": {
|
||||||
|
"global_seed": "Master reproducibility seed. Connect seed to generator seed and seed_config to seed_config so random choices can be recreated exactly.",
|
||||||
|
},
|
||||||
"SxCPSeedControl": {
|
"SxCPSeedControl": {
|
||||||
"category_seed_mode": "auto/follow_main follows the main seed; fixed uses category_seed; random rerolls this axis each queue.",
|
"category_seed_mode": "auto/follow_main follows the main seed; fixed uses category_seed; random rerolls this axis at queue time while the field value stays unchanged.",
|
||||||
"subcategory_seed_mode": "Controls which subcategory is selected. Change this to switch oral vs penetration when both are allowed.",
|
"subcategory_seed_mode": "Controls which subcategory is selected. Change this to switch oral vs penetration when both are allowed.",
|
||||||
"content_seed_mode": "Controls item/outfit content for non-pose categories.",
|
"content_seed_mode": "Controls item/outfit content for non-pose categories.",
|
||||||
"person_seed_mode": "Controls generated character appearance unless a slot seed overrides it.",
|
"person_seed_mode": "Controls generated character appearance unless a slot seed overrides it.",
|
||||||
@@ -178,7 +221,9 @@ NODE_INPUT_TOOLTIPS = {
|
|||||||
"composition_seed_mode": "Controls framing/composition text.",
|
"composition_seed_mode": "Controls framing/composition text.",
|
||||||
},
|
},
|
||||||
"SxCPSeedLocker": {
|
"SxCPSeedLocker": {
|
||||||
|
"base_seed": "Master seed for the locked result. Use the same value as the generator seed for simplest reproduction.",
|
||||||
"reroll_axis": "Choose the one axis to change while the rest stays locked. Use pose for sexual pose, scene for location, person for appearance.",
|
"reroll_axis": "Choose the one axis to change while the rest stays locked. Use pose for sexual pose, scene for location, person for appearance.",
|
||||||
|
"reroll_seed": "Seed for the selected axis only. Leave -1 to derive a stable reroll from base_seed.",
|
||||||
},
|
},
|
||||||
"SxCPCastBias": {
|
"SxCPCastBias": {
|
||||||
"seed": "Fixed cast-bias seed. Use -1 for a fresh cast each queue, or connect Global Seed/Seed Locker through seed_config.",
|
"seed": "Fixed cast-bias seed. Use -1 for a fresh cast each queue, or connect Global Seed/Seed Locker through seed_config.",
|
||||||
@@ -205,6 +250,7 @@ NODE_INPUT_TOOLTIPS = {
|
|||||||
"phone_visibility": "Use phone_hidden or suppress_phone_visibility when you do not want 'phone hidden' text in prompts.",
|
"phone_visibility": "Use phone_hidden or suppress_phone_visibility when you do not want 'phone hidden' text in prompts.",
|
||||||
},
|
},
|
||||||
"SxCPCameraOrbitControl": {
|
"SxCPCameraOrbitControl": {
|
||||||
|
"enabled": "When false, outputs an empty camera config so downstream nodes fall back to their own camera settings.",
|
||||||
"horizontal_angle": "Orbit angle in degrees. 0=front, 90=right side, 180=back, 270=left side.",
|
"horizontal_angle": "Orbit angle in degrees. 0=front, 90=right side, 180=back, 270=left side.",
|
||||||
"vertical_angle": "Camera elevation. Negative looks up, positive looks down.",
|
"vertical_angle": "Camera elevation. Negative looks up, positive looks down.",
|
||||||
"zoom": "Maps to distance/framing when framing is from_zoom.",
|
"zoom": "Maps to distance/framing when framing is from_zoom.",
|
||||||
@@ -215,6 +261,7 @@ NODE_INPUT_TOOLTIPS = {
|
|||||||
"qwen_prompt": "Camera prompt from Qwen MultiAngle, for example '<sks> front-right quarter view eye-level shot medium shot'.",
|
"qwen_prompt": "Camera prompt from Qwen MultiAngle, for example '<sks> front-right quarter view eye-level shot medium shot'.",
|
||||||
"camera_info": "Optional structured camera_info from Qwen MultiAngle. Used before qwen_prompt when prefer_camera_info is true.",
|
"camera_info": "Optional structured camera_info from Qwen MultiAngle. Used before qwen_prompt when prefer_camera_info is true.",
|
||||||
"prefer_camera_info": "Use structured camera_info values when available instead of parsing the text prompt.",
|
"prefer_camera_info": "Use structured camera_info values when available instead of parsing the text prompt.",
|
||||||
|
"phone_visibility": "Leave auto when using Qwen/Orbit camera prompts unless you explicitly want phone visibility text.",
|
||||||
"suppress_phone_visibility": "Avoid adding phone visibility text unless you explicitly set a phone option.",
|
"suppress_phone_visibility": "Avoid adding phone visibility text unless you explicitly set a phone option.",
|
||||||
},
|
},
|
||||||
"SxCPHardcorePositionPool": {
|
"SxCPHardcorePositionPool": {
|
||||||
@@ -242,7 +289,7 @@ NODE_INPUT_TOOLTIPS = {
|
|||||||
"hardcore_level": "Controls how explicit the hardcore prompt style is.",
|
"hardcore_level": "Controls how explicit the hardcore prompt style is.",
|
||||||
"platform_style": "Instagram/OnlyFans styling bias for the dual prompt pair.",
|
"platform_style": "Instagram/OnlyFans styling bias for the dual prompt pair.",
|
||||||
"continuity": "Whether the softcore and hardcore prompts share the room/creator setup.",
|
"continuity": "Whether the softcore and hardcore prompts share the room/creator setup.",
|
||||||
"hardcore_clothing_continuity": "How clothing carries from softcore to hardcore. explicit_nude omits clothing references.",
|
"hardcore_clothing_continuity": "How clothing carries from softcore to hardcore. explicit_nude avoids outfit references so clothing tokens do not fight nudity.",
|
||||||
"softcore_camera_mode": "Camera mode for the softcore prompt, or from_camera_config.",
|
"softcore_camera_mode": "Camera mode for the softcore prompt, or from_camera_config.",
|
||||||
"hardcore_camera_mode": "Camera mode for the hardcore prompt. same_as_softcore reuses the softcore setting.",
|
"hardcore_camera_mode": "Camera mode for the hardcore prompt. same_as_softcore reuses the softcore setting.",
|
||||||
"camera_detail": "Global camera verbosity for the pair unless a camera config overrides it.",
|
"camera_detail": "Global camera verbosity for the pair unless a camera config overrides it.",
|
||||||
@@ -256,6 +303,34 @@ NODE_INPUT_TOOLTIPS = {
|
|||||||
"SxCPPromptBuilderFromConfigs": {
|
"SxCPPromptBuilderFromConfigs": {
|
||||||
"seed": "Main seed. Connect Seed Config for per-axis control.",
|
"seed": "Main seed. Connect Seed Config for per-axis control.",
|
||||||
},
|
},
|
||||||
|
"SxCPCharacterSlot": {
|
||||||
|
"subject_type": "Choose whether this slot creates a woman or man. Man is required for POV presence.",
|
||||||
|
"label": "auto_chain uses the next free label for that subject type based on incoming cast order.",
|
||||||
|
"character_cast": "Optional incoming cast from the previous slot. Output this node's character_cast to the next slot or final generator.",
|
||||||
|
"presence_mode": "POV only has an effect for men; it makes the man implied by camera/body cues instead of fully described.",
|
||||||
|
"characteristics": "Optional controlled-random pool from age/body/eye/clothing nodes. Connected pools override the matching random choices.",
|
||||||
|
"hair_config": "Optional controlled-random hair pool. Chain Hair Length, Hair Color, and Hair Style before this slot.",
|
||||||
|
},
|
||||||
|
"SxCPWomanSlot": {
|
||||||
|
"label": "auto_chain uses the next free Woman label based on incoming cast order.",
|
||||||
|
"character_cast": "Optional incoming cast from the previous slot. Output this node's character_cast to the next slot or final generator.",
|
||||||
|
"figure_bias": "Broad woman body bias. Body Pool or manual body wording can narrow the actual phrase.",
|
||||||
|
"characteristics": "Optional controlled-random pool from age/body/eye/clothing nodes. Connected pools override the matching random choices.",
|
||||||
|
"hair_config": "Optional controlled-random hair pool. Chain Hair Length, Hair Color, and Hair Style before this slot.",
|
||||||
|
},
|
||||||
|
"SxCPManSlot": {
|
||||||
|
"label": "auto_chain uses the next free Man label based on incoming cast order.",
|
||||||
|
"character_cast": "Optional incoming cast from the previous slot. Output this node's character_cast to the next slot or final generator.",
|
||||||
|
"presence_mode": "visible describes the man normally; pov makes him the first-person viewer and suppresses most man descriptor text.",
|
||||||
|
"descriptor_detail": "compact or minimal usually works better for non-primary men; full makes the man as detailed as the creator.",
|
||||||
|
"characteristics": "Optional controlled-random pool from age/body/eye/clothing nodes. Connected pools override the matching random choices.",
|
||||||
|
"hair_config": "Optional controlled-random hair pool. Chain Hair Length, Hair Color, and Hair Style before this slot.",
|
||||||
|
},
|
||||||
|
"SxCPCharacterClothing": {
|
||||||
|
"softcore_source": "Built-in softcore outfit pool. custom reads custom_softcore_outfits; no_change leaves the current pool untouched.",
|
||||||
|
"hardcore_state": "Hardcore exposure pool. explicit_nude avoids outfit references; partially_removed can intentionally keep clothing words.",
|
||||||
|
"characteristics": "Incoming characteristic pool to extend or replace with clothing choices.",
|
||||||
|
},
|
||||||
"SxCPCharacterProfileSave": {
|
"SxCPCharacterProfileSave": {
|
||||||
"profile_name": "Profile filename stem. Saving requires save_now=true.",
|
"profile_name": "Profile filename stem. Saving requires save_now=true.",
|
||||||
"metadata_json": "Use generator metadata to save the currently generated character without regenerating it.",
|
"metadata_json": "Use generator metadata to save the currently generated character without regenerating it.",
|
||||||
@@ -297,6 +372,20 @@ NODE_INPUT_TOOLTIPS = {
|
|||||||
"SxCPAccumulator": {
|
"SxCPAccumulator": {
|
||||||
"image_batch_mode": "same_size_only keeps incompatible sizes separate; resize_to_first forces one image batch.",
|
"image_batch_mode": "same_size_only keeps incompatible sizes separate; resize_to_first forces one image batch.",
|
||||||
},
|
},
|
||||||
|
"SxCPAccumulatorPreview": {
|
||||||
|
"store_key": "Use the same key as the Accumulator store. Prefer wiring store_key_input from Accumulator to avoid mismatches.",
|
||||||
|
"view_mode": "grid shows all entries together; carousel keeps one large image visible for review.",
|
||||||
|
"zoom_level": "Visual preview scale only. It does not resize stored images or saved files.",
|
||||||
|
"delete_action": "Execution-time delete/clear. The preview JS buttons can also delete without changing this widget.",
|
||||||
|
"save_batch": "Saves all current accumulator images when the workflow executes and finished is true.",
|
||||||
|
"clear_after_save": "Clear the in-memory accumulator only after a successful save.",
|
||||||
|
},
|
||||||
|
"SxCPIndexSwitch": {
|
||||||
|
"mode": "pick_input selects input_N as value; route_output sends route_value to output_N.",
|
||||||
|
"index": "Selected input/output index. With one_based, index 1 maps to input_1/output_1.",
|
||||||
|
"missing_behavior": "Controls missing indexes: fallback uses fallback, clamp/wrap select another slot, none returns empty.",
|
||||||
|
"route_value": "Value sent to the selected output_N when mode is route_output.",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
OUTERCOURSE_BOOBJOB = "boobjob"
|
||||||
|
OUTERCOURSE_TESTICLE = "testicle_sucking"
|
||||||
|
OUTERCOURSE_PENIS_LICKING = "penis_licking"
|
||||||
|
OUTERCOURSE_HANDJOB = "handjob"
|
||||||
|
OUTERCOURSE_FOOTJOB = "footjob"
|
||||||
|
OUTERCOURSE_GENERIC = "generic"
|
||||||
|
|
||||||
|
|
||||||
|
OUTERCOURSE_ACTION_KIND_CHOICES = {
|
||||||
|
OUTERCOURSE_BOOBJOB,
|
||||||
|
OUTERCOURSE_TESTICLE,
|
||||||
|
OUTERCOURSE_PENIS_LICKING,
|
||||||
|
OUTERCOURSE_HANDJOB,
|
||||||
|
OUTERCOURSE_FOOTJOB,
|
||||||
|
OUTERCOURSE_GENERIC,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _clean(value: Any) -> str:
|
||||||
|
text = "" if value is None else str(value)
|
||||||
|
text = text.replace("\n", " ")
|
||||||
|
text = re.sub(r"\s+", " ", text).strip()
|
||||||
|
text = re.sub(r"\s+([,.;:])", r"\1", text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_outercourse_action_kind(value: Any, default: str = OUTERCOURSE_GENERIC) -> str:
|
||||||
|
text = re.sub(r"[^a-z0-9]+", "_", _clean(value).lower()).strip("_")
|
||||||
|
aliases = {
|
||||||
|
"breast_sex": OUTERCOURSE_BOOBJOB,
|
||||||
|
"titjob": OUTERCOURSE_BOOBJOB,
|
||||||
|
"tit_job": OUTERCOURSE_BOOBJOB,
|
||||||
|
"testicle": OUTERCOURSE_TESTICLE,
|
||||||
|
"testicles": OUTERCOURSE_TESTICLE,
|
||||||
|
"ball_licking": OUTERCOURSE_TESTICLE,
|
||||||
|
"balls_licking": OUTERCOURSE_TESTICLE,
|
||||||
|
"balls": OUTERCOURSE_TESTICLE,
|
||||||
|
"penis_lick": OUTERCOURSE_PENIS_LICKING,
|
||||||
|
"penis_tongue": OUTERCOURSE_PENIS_LICKING,
|
||||||
|
"hand_job": OUTERCOURSE_HANDJOB,
|
||||||
|
"two_handed_handjob": OUTERCOURSE_HANDJOB,
|
||||||
|
"foot_job": OUTERCOURSE_FOOTJOB,
|
||||||
|
"feet_job": OUTERCOURSE_FOOTJOB,
|
||||||
|
}
|
||||||
|
text = aliases.get(text, text)
|
||||||
|
return text if text in OUTERCOURSE_ACTION_KIND_CHOICES else default
|
||||||
|
|
||||||
|
|
||||||
|
def infer_outercourse_action_kind(*parts: Any) -> str:
|
||||||
|
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||||
|
if not text:
|
||||||
|
return OUTERCOURSE_GENERIC
|
||||||
|
if any(
|
||||||
|
term in text
|
||||||
|
for term in (
|
||||||
|
"boobjob",
|
||||||
|
"titjob",
|
||||||
|
"tit job",
|
||||||
|
"breast sex",
|
||||||
|
"breast-sex",
|
||||||
|
"breasts tightly around",
|
||||||
|
"breasts around",
|
||||||
|
"breasts firmly together",
|
||||||
|
"penis squeezed between both breasts",
|
||||||
|
"penis shaft compressed between breasts",
|
||||||
|
"soft flesh squeezed around the penis",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return OUTERCOURSE_BOOBJOB
|
||||||
|
if any(
|
||||||
|
term in text
|
||||||
|
for term in (
|
||||||
|
"testicle",
|
||||||
|
"balls licking",
|
||||||
|
"balls-licking",
|
||||||
|
"balls held",
|
||||||
|
"balls close",
|
||||||
|
"balls and mouth",
|
||||||
|
"mouth and tongue on the viewer's balls",
|
||||||
|
"mouth and tongue on the pov viewer's balls",
|
||||||
|
"mouth and tongue licking the viewer's balls",
|
||||||
|
"mouth and tongue licking the pov viewer's balls",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return OUTERCOURSE_TESTICLE
|
||||||
|
if any(
|
||||||
|
term in text
|
||||||
|
for term in (
|
||||||
|
"penis licking",
|
||||||
|
"penis-licking",
|
||||||
|
"tongue along",
|
||||||
|
"tongue runs along",
|
||||||
|
"tongue running along",
|
||||||
|
"tongue licking",
|
||||||
|
"underside of the penis",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return OUTERCOURSE_PENIS_LICKING
|
||||||
|
if any(
|
||||||
|
term in text
|
||||||
|
for term in (
|
||||||
|
"handjob",
|
||||||
|
"hand job",
|
||||||
|
"hand wrapped",
|
||||||
|
"hand wraps around",
|
||||||
|
"hand stroking",
|
||||||
|
"both hands stroking",
|
||||||
|
"two-handed",
|
||||||
|
"one hand grips",
|
||||||
|
"one hand wrapped around",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return OUTERCOURSE_HANDJOB
|
||||||
|
if any(
|
||||||
|
term in text
|
||||||
|
for term in (
|
||||||
|
"footjob",
|
||||||
|
"foot job",
|
||||||
|
"soles",
|
||||||
|
"toes curled",
|
||||||
|
"feet stroking",
|
||||||
|
"feet and penis",
|
||||||
|
"both feet",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return OUTERCOURSE_FOOTJOB
|
||||||
|
return OUTERCOURSE_GENERIC
|
||||||
|
|
||||||
|
|
||||||
|
def outercourse_context_text(*parts: Any) -> str:
|
||||||
|
return " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||||
+8
-9
@@ -7,11 +7,13 @@ try:
|
|||||||
from . import character_profile as character_profile_policy
|
from . import character_profile as character_profile_policy
|
||||||
from . import pair_clothing
|
from . import pair_clothing
|
||||||
from . import pair_options
|
from . import pair_options
|
||||||
|
from . import softcore_text_policy
|
||||||
except ImportError: # Allows local smoke tests with top-level imports.
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
import cast_context as cast_context_policy
|
import cast_context as cast_context_policy
|
||||||
import character_profile as character_profile_policy
|
import character_profile as character_profile_policy
|
||||||
import pair_clothing
|
import pair_clothing
|
||||||
import pair_options
|
import pair_options
|
||||||
|
import softcore_text_policy
|
||||||
|
|
||||||
|
|
||||||
AxisRng = Callable[[dict[str, int], str, int, int], Any]
|
AxisRng = Callable[[dict[str, int], str, int, int], Any]
|
||||||
@@ -264,16 +266,13 @@ def resolve_insta_pair_cast_context(
|
|||||||
else f"soft creator-teaser setup with {cast_summary_phrase(hard_women_count, hard_men_count)}"
|
else f"soft creator-teaser setup with {cast_summary_phrase(hard_women_count, hard_men_count)}"
|
||||||
)
|
)
|
||||||
soft_cast_presence = (
|
soft_cast_presence = (
|
||||||
(
|
softcore_text_policy.softcore_cast_presence_phrase(
|
||||||
"Frame Woman A from the POV participant's first-person camera in a soft creator-teaser setup; "
|
same_cast=same_softcore_cast,
|
||||||
"keep the POV participant off-camera as the viewpoint and implied by camera perspective or foreground cues. "
|
pov_labels=pov_character_labels,
|
||||||
)
|
cast_label="Woman A and the listed partners",
|
||||||
if same_softcore_cast and pov_character_labels
|
woman_label="Woman A",
|
||||||
else (
|
|
||||||
"Place Woman A and the listed partners together in a soft creator-teaser pose. "
|
|
||||||
if same_softcore_cast
|
|
||||||
else "Keep the softcore version focused on Woman A alone. "
|
|
||||||
)
|
)
|
||||||
|
+ ". "
|
||||||
)
|
)
|
||||||
soft_cast_styling_sentence = (
|
soft_cast_styling_sentence = (
|
||||||
f"Partner softcore styling: {soft_partner_outfit_text}. Cast pose: {soft_partner_styling['pose']}. "
|
f"Partner softcore styling: {soft_partner_outfit_text}. Cast pose: {soft_partner_styling['pose']}. "
|
||||||
|
|||||||
+26
-5
@@ -4,6 +4,11 @@ from dataclasses import dataclass
|
|||||||
import re
|
import re
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
WOMAN_LOWER_ACCESS_TERMS = (
|
WOMAN_LOWER_ACCESS_TERMS = (
|
||||||
"penetrat",
|
"penetrat",
|
||||||
@@ -169,6 +174,10 @@ def body_exposure_scene_text(scene: Any) -> str:
|
|||||||
(r",?\s*\blingerie visible nearby\b", ""),
|
(r",?\s*\blingerie visible nearby\b", ""),
|
||||||
(r"\boutfit racks\b", "mirror shelves"),
|
(r"\boutfit racks\b", "mirror shelves"),
|
||||||
(r"\bcostume racks\b", "mirror shelves"),
|
(r"\bcostume racks\b", "mirror shelves"),
|
||||||
|
(r"\bshoe shelves\b", "side shelves"),
|
||||||
|
(r"\bshoes visible\b", "body placement visible"),
|
||||||
|
(r"\bbag and shoes visible\b", "nearby floor edge visible"),
|
||||||
|
(r"\bshoes and bag visible\b", "nearby floor edge visible"),
|
||||||
(r"\bhanging outfits\b", "hanging fabric"),
|
(r"\bhanging outfits\b", "hanging fabric"),
|
||||||
(r"\bclothing hooks\b", "wall hooks"),
|
(r"\bclothing hooks\b", "wall hooks"),
|
||||||
(r"\boutfit-check\b", "creator-shot"),
|
(r"\boutfit-check\b", "creator-shot"),
|
||||||
@@ -233,7 +242,7 @@ def character_hardcore_clothing_entries(
|
|||||||
|
|
||||||
def hardcore_row_access_flags(row: dict[str, Any]) -> dict[str, bool]:
|
def hardcore_row_access_flags(row: dict[str, Any]) -> dict[str, bool]:
|
||||||
axis_values = row.get("item_axis_values")
|
axis_values = row.get("item_axis_values")
|
||||||
axis_text = " ".join(str(value) for value in axis_values.values()) if isinstance(axis_values, dict) else ""
|
axis_text = item_axis_policy.context_text(axis_values=axis_values)
|
||||||
role_text = " ".join(
|
role_text = " ".join(
|
||||||
str(part or "")
|
str(part or "")
|
||||||
for part in (
|
for part in (
|
||||||
@@ -251,10 +260,11 @@ def hardcore_row_access_flags(row: dict[str, Any]) -> dict[str, bool]:
|
|||||||
)
|
)
|
||||||
).lower()
|
).lower()
|
||||||
full_text = f"{role_text} {detail_text}"
|
full_text = f"{role_text} {detail_text}"
|
||||||
|
lower_access_text = f"{role_text} {axis_text}"
|
||||||
return {
|
return {
|
||||||
"woman_lower": any(term in role_text for term in WOMAN_LOWER_ACCESS_TERMS),
|
"woman_lower": any(term in lower_access_text for term in WOMAN_LOWER_ACCESS_TERMS),
|
||||||
"woman_upper": any(term in full_text for term in WOMAN_UPPER_ACCESS_TERMS),
|
"woman_upper": any(term in full_text for term in WOMAN_UPPER_ACCESS_TERMS),
|
||||||
"man_lower": any(term in role_text for term in MAN_LOWER_ACCESS_TERMS),
|
"man_lower": any(term in lower_access_text for term in MAN_LOWER_ACCESS_TERMS),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -433,6 +443,17 @@ def resolve_hardcore_pair_clothing_result(
|
|||||||
if str(part or "").strip()
|
if str(part or "").strip()
|
||||||
]
|
]
|
||||||
hard_clothing_state = "; ".join(hard_clothing_parts)
|
hard_clothing_state = "; ".join(hard_clothing_parts)
|
||||||
|
scene_cleanup_terms = (
|
||||||
|
"body is fully exposed",
|
||||||
|
"bare skin unobstructed",
|
||||||
|
"body is partly exposed",
|
||||||
|
"lower body is clear",
|
||||||
|
"upper body are clear",
|
||||||
|
"pulled aside",
|
||||||
|
"removed below the hips",
|
||||||
|
"pants and underwear are pulled down",
|
||||||
|
)
|
||||||
|
hard_clothing_lower = hard_clothing_state.lower()
|
||||||
return HardcorePairClothingRoute(
|
return HardcorePairClothingRoute(
|
||||||
access_flags=access_flags,
|
access_flags=access_flags,
|
||||||
woman_access=woman_access,
|
woman_access=woman_access,
|
||||||
@@ -440,8 +461,8 @@ def resolve_hardcore_pair_clothing_result(
|
|||||||
hardcore_clothing_state=hard_clothing_state,
|
hardcore_clothing_state=hard_clothing_state,
|
||||||
hardcore_clothing_sentence=f"{hard_clothing_state}. " if hard_clothing_state else "",
|
hardcore_clothing_sentence=f"{hard_clothing_state}. " if hard_clothing_state else "",
|
||||||
requires_body_exposure_scene=(
|
requires_body_exposure_scene=(
|
||||||
"body is fully exposed" in hard_clothing_state.lower()
|
any(access_flags.values())
|
||||||
or "bare skin unobstructed" in hard_clothing_state.lower()
|
or any(term in hard_clothing_lower for term in scene_cleanup_terms)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -4,6 +4,11 @@ import json
|
|||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import category_library as category_policy
|
||||||
|
except ImportError: # Allows local smoke tests from the repository root.
|
||||||
|
import category_library as category_policy
|
||||||
|
|
||||||
|
|
||||||
INSTA_OF_SOFT_LEVELS = {
|
INSTA_OF_SOFT_LEVELS = {
|
||||||
"social_tease": "Instagram-style thirst-trap post, suggestive polished social feed energy",
|
"social_tease": "Instagram-style thirst-trap post, suggestive polished social feed energy",
|
||||||
@@ -409,7 +414,11 @@ def softcore_category(level: str) -> tuple[str, str]:
|
|||||||
level,
|
level,
|
||||||
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL["lingerie_tease"],
|
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL["lingerie_tease"],
|
||||||
)
|
)
|
||||||
category, _subcategory = subcategory.split(" / ", 1)
|
exact_choice = category_policy.split_exact_subcategory_choice(
|
||||||
|
category_policy.load_category_library(),
|
||||||
|
subcategory,
|
||||||
|
)
|
||||||
|
category = exact_choice[0]["name"] if exact_choice else subcategory.split(" / ", 1)[0]
|
||||||
return category, subcategory
|
return category, subcategory
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -4,8 +4,10 @@ from typing import Any, Callable
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from . import row_normalization as row_policy
|
from . import row_normalization as row_policy
|
||||||
|
from . import softcore_text_policy
|
||||||
except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.py`.
|
except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.py`.
|
||||||
import row_normalization as row_policy
|
import row_normalization as row_policy
|
||||||
|
import softcore_text_policy
|
||||||
|
|
||||||
|
|
||||||
def _labeled_expression_sentence(label: str, expression: Any) -> str:
|
def _labeled_expression_sentence(label: str, expression: Any) -> str:
|
||||||
@@ -84,7 +86,7 @@ def assemble_insta_pair_metadata(
|
|||||||
f"{_labeled_expression_sentence('Facial expression', soft_row.get('expression'))}"
|
f"{_labeled_expression_sentence('Facial expression', soft_row.get('expression'))}"
|
||||||
f"Composition: {soft_row['composition']}. "
|
f"Composition: {soft_row['composition']}. "
|
||||||
f"{soft_camera_sentence}"
|
f"{soft_camera_sentence}"
|
||||||
"Keep the softcore version seductive, creator-shot, and styled as a soft teaser. "
|
f"{softcore_text_policy.softcore_style_directive()} "
|
||||||
f"{soft_row['positive_suffix']}."
|
f"{soft_row['positive_suffix']}."
|
||||||
)
|
)
|
||||||
hard_prompt = (
|
hard_prompt = (
|
||||||
|
|||||||
+117
-150
@@ -5,6 +5,8 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from . import builder_config_route as builder_config_route_policy
|
||||||
|
from . import builder_prompt_route as builder_prompt_route_policy
|
||||||
from .category_library import (
|
from .category_library import (
|
||||||
compatible_entries as _compatible_entries,
|
compatible_entries as _compatible_entries,
|
||||||
compatible_entry as _compatible_entry,
|
compatible_entry as _compatible_entry,
|
||||||
@@ -51,6 +53,8 @@ try:
|
|||||||
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
||||||
)
|
)
|
||||||
except ImportError: # Allows local smoke tests with `python -c`.
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
|
import builder_config_route as builder_config_route_policy
|
||||||
|
import builder_prompt_route as builder_prompt_route_policy
|
||||||
from category_library import (
|
from category_library import (
|
||||||
compatible_entries as _compatible_entries,
|
compatible_entries as _compatible_entries,
|
||||||
compatible_entry as _compatible_entry,
|
compatible_entry as _compatible_entry,
|
||||||
@@ -302,6 +306,10 @@ def _outercourse_axis_values_for_position(values: list[Any], position: str, axis
|
|||||||
return row_item_policy.outercourse_axis_values_for_position(values, position, axis_name)
|
return row_item_policy.outercourse_axis_values_for_position(values, position, axis_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _anal_axis_values_for_position(values: list[Any], position: str, axis_name: str) -> list[Any]:
|
||||||
|
return row_item_policy.anal_axis_values_for_position(values, position, axis_name)
|
||||||
|
|
||||||
|
|
||||||
def _compose_item(
|
def _compose_item(
|
||||||
rng: random.Random,
|
rng: random.Random,
|
||||||
category: dict[str, Any],
|
category: dict[str, Any],
|
||||||
@@ -352,6 +360,18 @@ def seed_mode_choices() -> list[str]:
|
|||||||
return seed_policy.seed_mode_choices()
|
return seed_policy.seed_mode_choices()
|
||||||
|
|
||||||
|
|
||||||
|
def seed_reroll_axis_choices() -> list[str]:
|
||||||
|
return seed_policy.seed_reroll_axis_choices()
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_seed_mode(value: Any) -> str:
|
||||||
|
return seed_policy.normalize_seed_mode(value)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_reroll_axis(value: Any) -> str:
|
||||||
|
return seed_policy.normalize_reroll_axis(value)
|
||||||
|
|
||||||
|
|
||||||
CATEGORY_PRESETS = category_cast_policy.CATEGORY_PRESETS
|
CATEGORY_PRESETS = category_cast_policy.CATEGORY_PRESETS
|
||||||
CAST_PRESETS = category_cast_policy.CAST_PRESETS
|
CAST_PRESETS = category_cast_policy.CAST_PRESETS
|
||||||
|
|
||||||
@@ -2376,6 +2396,7 @@ def _build_custom_row(
|
|||||||
)
|
)
|
||||||
scene_slug = prompt_axes.scene_slug
|
scene_slug = prompt_axes.scene_slug
|
||||||
scene = prompt_axes.scene
|
scene = prompt_axes.scene
|
||||||
|
scene_entry = dict(prompt_axes.scene_entry)
|
||||||
pose = prompt_axes.pose
|
pose = prompt_axes.pose
|
||||||
expression = prompt_axes.expression
|
expression = prompt_axes.expression
|
||||||
shared_expression = prompt_axes.shared_expression
|
shared_expression = prompt_axes.shared_expression
|
||||||
@@ -2383,6 +2404,7 @@ def _build_custom_row(
|
|||||||
character_expression_text = prompt_axes.character_expression_text
|
character_expression_text = prompt_axes.character_expression_text
|
||||||
source_composition = prompt_axes.source_composition
|
source_composition = prompt_axes.source_composition
|
||||||
composition = prompt_axes.composition
|
composition = prompt_axes.composition
|
||||||
|
composition_entry = dict(prompt_axes.composition_entry)
|
||||||
action_route = _action_position_route(
|
action_route = _action_position_route(
|
||||||
is_pose_category=is_pose_category,
|
is_pose_category=is_pose_category,
|
||||||
subcategory=subcategory,
|
subcategory=subcategory,
|
||||||
@@ -2420,6 +2442,7 @@ def _build_custom_row(
|
|||||||
negative_prompt=text_fields.negative_prompt,
|
negative_prompt=text_fields.negative_prompt,
|
||||||
scene_slug=scene_slug,
|
scene_slug=scene_slug,
|
||||||
scene=scene,
|
scene=scene,
|
||||||
|
scene_entry=scene_entry,
|
||||||
pose=pose,
|
pose=pose,
|
||||||
expression=expression,
|
expression=expression,
|
||||||
shared_expression=shared_expression,
|
shared_expression=shared_expression,
|
||||||
@@ -2430,6 +2453,7 @@ def _build_custom_row(
|
|||||||
expression_intensity_source=expression_intensity_source,
|
expression_intensity_source=expression_intensity_source,
|
||||||
composition=composition,
|
composition=composition,
|
||||||
source_composition=source_composition,
|
source_composition=source_composition,
|
||||||
|
composition_entry=composition_entry,
|
||||||
role_graph=role_graph,
|
role_graph=role_graph,
|
||||||
source_role_graph=source_role_graph,
|
source_role_graph=source_role_graph,
|
||||||
action_family=action_family,
|
action_family=action_family,
|
||||||
@@ -2458,6 +2482,35 @@ def _build_custom_row(
|
|||||||
return _assemble_custom_row(assembly_request)
|
return _assemble_custom_row(assembly_request)
|
||||||
|
|
||||||
|
|
||||||
|
def _prompt_build_dependencies() -> builder_prompt_route_policy.PromptBuildDependencies:
|
||||||
|
return builder_prompt_route_policy.PromptBuildDependencies(
|
||||||
|
default_trigger=g.TRIGGER,
|
||||||
|
default_negative=g.NEGATIVE_PROMPT,
|
||||||
|
random_subcategory=RANDOM_SUBCATEGORY,
|
||||||
|
apply_pool_extensions=apply_pool_extensions,
|
||||||
|
normalize_ethnicity_filter=normalize_ethnicity_filter,
|
||||||
|
is_false=_is_false,
|
||||||
|
ratio_or_none=_ratio_or_none,
|
||||||
|
parse_seed_config=_parse_seed_config,
|
||||||
|
parse_location_config=_parse_location_config,
|
||||||
|
parse_composition_config=_parse_composition_config,
|
||||||
|
axis_rng=_axis_rng,
|
||||||
|
pick_clothing_mode=_pick_clothing_mode,
|
||||||
|
pick_pose_mode=_pick_pose_mode,
|
||||||
|
pick_figure_bias=_pick_figure_bias,
|
||||||
|
pick_expression_intensity=_pick_expression_intensity,
|
||||||
|
auto_full_choice=_auto_full_choice,
|
||||||
|
build_auto_weighted_row=_build_auto_weighted_row,
|
||||||
|
build_direct_builtin_row=_build_direct_builtin_row,
|
||||||
|
build_custom_row=_build_custom_row,
|
||||||
|
apply_location_config_to_legacy_row=row_location_policy.apply_location_config_to_legacy_row,
|
||||||
|
apply_composition_config_to_legacy_row=row_location_policy.apply_composition_config_to_legacy_row,
|
||||||
|
disable_row_expression=_disable_row_expression,
|
||||||
|
apply_camera_config=_apply_camera_config,
|
||||||
|
normalize_prompt_row=row_policy.normalize_prompt_row,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_prompt(
|
def build_prompt(
|
||||||
category: str,
|
category: str,
|
||||||
subcategory: str,
|
subcategory: str,
|
||||||
@@ -2490,123 +2543,51 @@ def build_prompt(
|
|||||||
location_config: str | dict[str, Any] | None = None,
|
location_config: str | dict[str, Any] | None = None,
|
||||||
composition_config: str | dict[str, Any] | None = None,
|
composition_config: str | dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
apply_pool_extensions()
|
return builder_prompt_route_policy.build_prompt(
|
||||||
row_number = max(1, int(row_number))
|
builder_prompt_route_policy.PromptBuildRequest(
|
||||||
start_index = max(1, int(start_index))
|
category=category,
|
||||||
seed = int(seed)
|
subcategory=subcategory,
|
||||||
ethnicity = normalize_ethnicity_filter(ethnicity, "any")
|
row_number=row_number,
|
||||||
expression_enabled = not _is_false(expression_enabled)
|
start_index=start_index,
|
||||||
minimal_ratio = _ratio_or_none(minimal_clothing_ratio)
|
seed=seed,
|
||||||
pose_ratio = _ratio_or_none(standard_pose_ratio)
|
clothing=clothing,
|
||||||
parsed_seed_config = _parse_seed_config(seed_config)
|
ethnicity=ethnicity,
|
||||||
parsed_location_config = _parse_location_config(location_config)
|
poses=poses,
|
||||||
parsed_composition_config = _parse_composition_config(composition_config)
|
backside_bias=backside_bias,
|
||||||
content_rng = _axis_rng(parsed_seed_config, "content", seed, row_number)
|
figure=figure,
|
||||||
pose_axis_rng = _axis_rng(parsed_seed_config, "pose", seed, row_number)
|
no_plus_women=no_plus_women,
|
||||||
person_rng = _axis_rng(parsed_seed_config, "person", seed, row_number)
|
no_black=no_black,
|
||||||
expression_rng = _axis_rng(parsed_seed_config, "expression", seed, row_number)
|
minimal_clothing_ratio=minimal_clothing_ratio,
|
||||||
clothing = clothing if clothing in ("full", "minimal", "random") else "full"
|
standard_pose_ratio=standard_pose_ratio,
|
||||||
poses = poses if poses in ("standard", "evocative", "random") else "standard"
|
trigger=trigger,
|
||||||
figure = figure if figure in ("curvy", "balanced", "bombshell", "random") else "curvy"
|
prepend_trigger_to_prompt=prepend_trigger_to_prompt,
|
||||||
clothing = _pick_clothing_mode(content_rng, clothing, minimal_ratio)
|
extra_positive=extra_positive,
|
||||||
poses = _pick_pose_mode(pose_axis_rng, poses, pose_ratio)
|
extra_negative=extra_negative,
|
||||||
figure = _pick_figure_bias(person_rng, figure)
|
seed_config=seed_config,
|
||||||
minimal_ratio = None
|
women_count=women_count,
|
||||||
pose_ratio = None
|
men_count=men_count,
|
||||||
expression_intensity, expression_intensity_source = _pick_expression_intensity(expression_rng, expression_intensity)
|
camera_config=camera_config,
|
||||||
|
expression_intensity=expression_intensity,
|
||||||
exact_custom_subcategory = bool(subcategory and subcategory != RANDOM_SUBCATEGORY and " / " in subcategory)
|
character_profile=character_profile,
|
||||||
|
character_cast=character_cast,
|
||||||
if category == "auto_full" and not exact_custom_subcategory:
|
expression_enabled=expression_enabled,
|
||||||
category = _auto_full_choice(parsed_seed_config, seed, row_number)
|
expression_phase=expression_phase,
|
||||||
|
hardcore_position_config=hardcore_position_config,
|
||||||
if category == "auto_weighted" and not exact_custom_subcategory:
|
location_config=location_config,
|
||||||
row = _build_auto_weighted_row(
|
composition_config=composition_config,
|
||||||
row_number,
|
),
|
||||||
start_index,
|
_prompt_build_dependencies(),
|
||||||
clothing,
|
)
|
||||||
ethnicity,
|
|
||||||
poses,
|
|
||||||
float(backside_bias),
|
def _prompt_from_configs_dependencies() -> builder_config_route_policy.PromptFromConfigsDependencies:
|
||||||
figure,
|
return builder_config_route_policy.PromptFromConfigsDependencies(
|
||||||
bool(no_plus_women),
|
parse_category_config=_parse_category_config,
|
||||||
bool(no_black),
|
parse_cast_config=_parse_cast_config,
|
||||||
minimal_ratio,
|
parse_generation_profile=_parse_generation_profile,
|
||||||
pose_ratio,
|
parse_filter_config=_parse_filter_config,
|
||||||
seed,
|
build_prompt=build_prompt,
|
||||||
)
|
|
||||||
elif category in ("woman", "man", "couple", "group_or_layout") and not exact_custom_subcategory:
|
|
||||||
row = _build_direct_builtin_row(
|
|
||||||
category,
|
|
||||||
row_number,
|
|
||||||
start_index,
|
|
||||||
clothing,
|
|
||||||
ethnicity,
|
|
||||||
poses,
|
|
||||||
float(backside_bias),
|
|
||||||
figure,
|
|
||||||
bool(no_plus_women),
|
|
||||||
bool(no_black),
|
|
||||||
minimal_ratio,
|
|
||||||
pose_ratio,
|
|
||||||
seed,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
row = _build_custom_row(
|
|
||||||
category,
|
|
||||||
subcategory,
|
|
||||||
row_number,
|
|
||||||
start_index,
|
|
||||||
ethnicity,
|
|
||||||
poses,
|
|
||||||
figure,
|
|
||||||
bool(no_plus_women),
|
|
||||||
bool(no_black),
|
|
||||||
int(women_count),
|
|
||||||
int(men_count),
|
|
||||||
seed,
|
|
||||||
parsed_seed_config,
|
|
||||||
expression_enabled,
|
|
||||||
expression_intensity,
|
|
||||||
expression_intensity_source,
|
|
||||||
character_profile,
|
|
||||||
character_cast,
|
|
||||||
expression_phase,
|
|
||||||
hardcore_position_config,
|
|
||||||
parsed_location_config,
|
|
||||||
parsed_composition_config,
|
|
||||||
)
|
|
||||||
|
|
||||||
if row.get("source") == "built_in_generator":
|
|
||||||
row = row_location_policy.apply_location_config_to_legacy_row(
|
|
||||||
row,
|
|
||||||
parsed_location_config,
|
|
||||||
parsed_seed_config,
|
|
||||||
seed,
|
|
||||||
row_number,
|
|
||||||
)
|
|
||||||
row = row_location_policy.apply_composition_config_to_legacy_row(
|
|
||||||
row,
|
|
||||||
parsed_composition_config,
|
|
||||||
parsed_seed_config,
|
|
||||||
seed,
|
|
||||||
row_number,
|
|
||||||
)
|
|
||||||
if not expression_enabled:
|
|
||||||
row = _disable_row_expression(row, "disabled")
|
|
||||||
row = _apply_camera_config(row, camera_config)
|
|
||||||
active_trigger = trigger.strip() or g.TRIGGER
|
|
||||||
row = row_policy.normalize_prompt_row(
|
|
||||||
row,
|
|
||||||
active_trigger=active_trigger,
|
|
||||||
prepend_trigger_to_prompt=bool(prepend_trigger_to_prompt),
|
|
||||||
extra_positive=extra_positive,
|
|
||||||
extra_negative=extra_negative,
|
|
||||||
default_negative=g.NEGATIVE_PROMPT,
|
|
||||||
)
|
)
|
||||||
row.setdefault("expression_intensity", expression_intensity)
|
|
||||||
row.setdefault("expression_intensity_source", expression_intensity_source)
|
|
||||||
return row
|
|
||||||
|
|
||||||
|
|
||||||
def build_prompt_from_configs(
|
def build_prompt_from_configs(
|
||||||
@@ -2627,40 +2608,26 @@ def build_prompt_from_configs(
|
|||||||
extra_positive: str = "",
|
extra_positive: str = "",
|
||||||
extra_negative: str = "",
|
extra_negative: str = "",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
category, subcategory = _parse_category_config(category_config)
|
return builder_config_route_policy.build_prompt_from_configs(
|
||||||
cast = _parse_cast_config(cast_config)
|
builder_config_route_policy.PromptFromConfigsRequest(
|
||||||
profile = _parse_generation_profile(generation_profile)
|
row_number=row_number,
|
||||||
filters = _parse_filter_config(filter_config)
|
start_index=start_index,
|
||||||
return build_prompt(
|
seed=seed,
|
||||||
category=category,
|
category_config=category_config,
|
||||||
subcategory=subcategory,
|
cast_config=cast_config,
|
||||||
row_number=row_number,
|
generation_profile=generation_profile,
|
||||||
start_index=start_index,
|
filter_config=filter_config,
|
||||||
seed=seed,
|
seed_config=seed_config,
|
||||||
clothing=profile["clothing"],
|
camera_config=camera_config,
|
||||||
ethnicity=filters["ethnicity"],
|
character_profile=character_profile,
|
||||||
poses=profile["poses"],
|
character_cast=character_cast,
|
||||||
expression_enabled=profile["expression_enabled"],
|
hardcore_position_config=hardcore_position_config,
|
||||||
expression_intensity=profile["expression_intensity"],
|
location_config=location_config,
|
||||||
backside_bias=profile["backside_bias"],
|
composition_config=composition_config,
|
||||||
figure=filters["figure"],
|
extra_positive=extra_positive,
|
||||||
no_plus_women=filters["no_plus_women"],
|
extra_negative=extra_negative,
|
||||||
no_black=filters["no_black"],
|
),
|
||||||
women_count=int(cast["women_count"]),
|
_prompt_from_configs_dependencies(),
|
||||||
men_count=int(cast["men_count"]),
|
|
||||||
minimal_clothing_ratio=profile["minimal_clothing_ratio"],
|
|
||||||
standard_pose_ratio=profile["standard_pose_ratio"],
|
|
||||||
trigger=profile["trigger"],
|
|
||||||
prepend_trigger_to_prompt=profile["prepend_trigger_to_prompt"],
|
|
||||||
extra_positive=extra_positive or "",
|
|
||||||
extra_negative=extra_negative or "",
|
|
||||||
seed_config=seed_config or "",
|
|
||||||
camera_config=camera_config or "",
|
|
||||||
character_profile=character_profile or "",
|
|
||||||
character_cast=character_cast or "",
|
|
||||||
hardcore_position_config=hardcore_position_config or "",
|
|
||||||
location_config=location_config or "",
|
|
||||||
composition_config=composition_config or "",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -167,3 +167,8 @@ def sanitize_tag_prompt(value: Any, triggers: Iterable[str] = ()) -> str:
|
|||||||
|
|
||||||
def sanitize_negative_text(value: Any) -> str:
|
def sanitize_negative_text(value: Any) -> str:
|
||||||
return dedupe_comma_list(value)
|
return dedupe_comma_list(value)
|
||||||
|
|
||||||
|
|
||||||
|
def combine_negative_text(*parts: Any) -> str:
|
||||||
|
cleaned = [clean_spacing(part).strip(" ,.;") for part in parts if clean_spacing(part).strip(" ,.;")]
|
||||||
|
return sanitize_negative_text(", ".join(cleaned))
|
||||||
|
|||||||
+32
-3
@@ -16,13 +16,29 @@ except ImportError: # Allows local smoke tests from the repository root.
|
|||||||
def row_action_family(row: Any, default: str = "") -> str:
|
def row_action_family(row: Any, default: str = "") -> str:
|
||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
return default
|
return default
|
||||||
return normalize_hardcore_action_family(row.get("action_family"), default)
|
family = normalize_hardcore_action_family(row.get("action_family"), "")
|
||||||
|
if family:
|
||||||
|
return family
|
||||||
|
metadata = row.get("item_template_metadata")
|
||||||
|
if isinstance(metadata, dict):
|
||||||
|
family = template_metadata_policy.template_action_family(metadata)
|
||||||
|
if family:
|
||||||
|
return family
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def row_position_family(row: Any, default: str = "") -> str:
|
def row_position_family(row: Any, default: str = "") -> str:
|
||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
return default
|
return default
|
||||||
return normalize_hardcore_position_family(str(row.get("position_family") or "").strip().lower(), default)
|
family = normalize_hardcore_position_family(str(row.get("position_family") or "").strip().lower(), "")
|
||||||
|
if family:
|
||||||
|
return family
|
||||||
|
metadata = row.get("item_template_metadata")
|
||||||
|
if isinstance(metadata, dict):
|
||||||
|
family = template_metadata_policy.template_position_family(metadata)
|
||||||
|
if family:
|
||||||
|
return family
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def _raw_position_key_values(row: dict[str, Any]) -> list[Any]:
|
def _raw_position_key_values(row: dict[str, Any]) -> list[Any]:
|
||||||
@@ -49,6 +65,11 @@ def row_position_keys(row: Any, *, include_unknown: bool = False) -> list[str]:
|
|||||||
return []
|
return []
|
||||||
values = _raw_position_key_values(row)
|
values = _raw_position_key_values(row)
|
||||||
selected = normalize_hardcore_position_values(values)
|
selected = normalize_hardcore_position_values(values)
|
||||||
|
metadata = row.get("item_template_metadata")
|
||||||
|
if isinstance(metadata, dict):
|
||||||
|
for key in template_metadata_policy.template_position_keys(metadata):
|
||||||
|
if key and key not in selected:
|
||||||
|
selected.append(key)
|
||||||
if not include_unknown:
|
if not include_unknown:
|
||||||
return selected
|
return selected
|
||||||
for value in values:
|
for value in values:
|
||||||
@@ -59,4 +80,12 @@ def row_position_keys(row: Any, *, include_unknown: bool = False) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def row_formatter_hints(row: Any, route: str) -> list[str]:
|
def row_formatter_hints(row: Any, route: str) -> list[str]:
|
||||||
return template_metadata_policy.formatter_hints_for_route(row, route)
|
hints: list[str] = []
|
||||||
|
for hint in template_metadata_policy.formatter_hints_for_route(row, route):
|
||||||
|
if hint not in hints:
|
||||||
|
hints.append(hint)
|
||||||
|
if isinstance(row, dict) and isinstance(row.get("item_template_metadata"), dict):
|
||||||
|
for hint in template_metadata_policy.formatter_hints_for_route(row["item_template_metadata"], route):
|
||||||
|
if hint not in hints:
|
||||||
|
hints.append(hint)
|
||||||
|
return hints
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class CustomRowAssemblyRequest:
|
|||||||
negative_prompt: str
|
negative_prompt: str
|
||||||
scene_slug: str
|
scene_slug: str
|
||||||
scene: str
|
scene: str
|
||||||
|
scene_entry: dict[str, Any]
|
||||||
pose: str
|
pose: str
|
||||||
expression: str
|
expression: str
|
||||||
shared_expression: str
|
shared_expression: str
|
||||||
@@ -47,6 +48,7 @@ class CustomRowAssemblyRequest:
|
|||||||
expression_intensity_source: str
|
expression_intensity_source: str
|
||||||
composition: str
|
composition: str
|
||||||
source_composition: str
|
source_composition: str
|
||||||
|
composition_entry: dict[str, Any]
|
||||||
role_graph: str
|
role_graph: str
|
||||||
source_role_graph: str
|
source_role_graph: str
|
||||||
action_family: str
|
action_family: str
|
||||||
@@ -85,6 +87,7 @@ def assemble_custom_row(request: CustomRowAssemblyRequest) -> dict[str, Any]:
|
|||||||
"style": r.style,
|
"style": r.style,
|
||||||
"scene": r.scene,
|
"scene": r.scene,
|
||||||
"scene_slug": r.scene_slug,
|
"scene_slug": r.scene_slug,
|
||||||
|
"scene_entry": r.scene_entry,
|
||||||
"pose": r.pose,
|
"pose": r.pose,
|
||||||
"expression": r.expression,
|
"expression": r.expression,
|
||||||
"shared_expression": r.shared_expression,
|
"shared_expression": r.shared_expression,
|
||||||
@@ -95,6 +98,7 @@ def assemble_custom_row(request: CustomRowAssemblyRequest) -> dict[str, Any]:
|
|||||||
"expression_intensity": r.expression_intensity,
|
"expression_intensity": r.expression_intensity,
|
||||||
"expression_intensity_source": r.expression_intensity_source,
|
"expression_intensity_source": r.expression_intensity_source,
|
||||||
"composition": r.composition,
|
"composition": r.composition,
|
||||||
|
"composition_entry": r.composition_entry,
|
||||||
"source_composition": r.source_composition,
|
"source_composition": r.source_composition,
|
||||||
"composition_prompt": row_camera_policy.composition_prompt(r.composition),
|
"composition_prompt": row_camera_policy.composition_prompt(r.composition),
|
||||||
"composition_config": r.composition_config or {},
|
"composition_config": r.composition_config or {},
|
||||||
@@ -156,6 +160,13 @@ def assemble_custom_row(request: CustomRowAssemblyRequest) -> dict[str, Any]:
|
|||||||
"item_template_metadata": r.item_template_metadata,
|
"item_template_metadata": r.item_template_metadata,
|
||||||
"formatter_hints": r.formatter_hints,
|
"formatter_hints": r.formatter_hints,
|
||||||
"scene_text": r.scene,
|
"scene_text": r.scene,
|
||||||
|
"scene_entry": r.scene_entry,
|
||||||
|
"location_theme": (r.location_config or {}).get("theme", ""),
|
||||||
|
"scene_theme": r.scene_entry.get("theme", "") or (
|
||||||
|
(r.location_config or {}).get("theme", "")
|
||||||
|
if (r.location_config or {}).get("apply_mode") == "replace"
|
||||||
|
else ""
|
||||||
|
),
|
||||||
"location_config": r.location_config or {},
|
"location_config": r.location_config or {},
|
||||||
"pose": r.pose,
|
"pose": r.pose,
|
||||||
"seed_config": r.seed_config,
|
"seed_config": r.seed_config,
|
||||||
@@ -168,6 +179,8 @@ def assemble_custom_row(request: CustomRowAssemblyRequest) -> dict[str, Any]:
|
|||||||
"position_key": r.position_key,
|
"position_key": r.position_key,
|
||||||
"position_keys": r.position_keys,
|
"position_keys": r.position_keys,
|
||||||
"source_composition": r.source_composition,
|
"source_composition": r.source_composition,
|
||||||
|
"composition_entry": r.composition_entry,
|
||||||
|
"composition_theme": (r.composition_config or {}).get("theme", ""),
|
||||||
"pov_character_labels": r.pov_character_labels,
|
"pov_character_labels": r.pov_character_labels,
|
||||||
"pov_prompt_directive": pov_prompt_directive,
|
"pov_prompt_directive": pov_prompt_directive,
|
||||||
"shared_expression": r.shared_expression,
|
"shared_expression": r.shared_expression,
|
||||||
|
|||||||
+64
-3
@@ -47,10 +47,29 @@ def coworking_composition_prompt(scene_text: Any, composition: Any, subject_kind
|
|||||||
return scene_camera_adapters.coworking_composition_prompt(scene_text, composition, subject_kind)
|
return scene_camera_adapters.coworking_composition_prompt(scene_text, composition, subject_kind)
|
||||||
|
|
||||||
|
|
||||||
|
def row_scene_text(row: dict[str, Any]) -> Any:
|
||||||
|
return row.get("scene_text") or row.get("source_scene_text") or row.get("scene")
|
||||||
|
|
||||||
|
|
||||||
|
def row_scene_theme(row: dict[str, Any]) -> str:
|
||||||
|
return str(row.get("scene_theme") or row.get("location_theme") or "")
|
||||||
|
|
||||||
|
|
||||||
|
def row_scene_profile_key(row: dict[str, Any]) -> str:
|
||||||
|
return str(row.get("scene_camera_profile_key") or "")
|
||||||
|
|
||||||
|
|
||||||
def apply_contextual_composition(row: dict[str, Any], subject_kind: str) -> dict[str, Any]:
|
def apply_contextual_composition(row: dict[str, Any], subject_kind: str) -> dict[str, Any]:
|
||||||
scene_text = row.get("scene_text") or row.get("source_scene_text") or row.get("scene")
|
scene_text = row_scene_text(row)
|
||||||
old_composition = str(row.get("composition") or "").strip()
|
old_composition = str(row.get("composition") or "").strip()
|
||||||
new_composition = coworking_composition_prompt(scene_text, old_composition, subject_kind)
|
new_composition = scene_camera_adapters.contextual_composition_prompt(
|
||||||
|
scene_text,
|
||||||
|
old_composition,
|
||||||
|
subject_kind,
|
||||||
|
scene_entry=row.get("scene_entry"),
|
||||||
|
theme=row_scene_theme(row),
|
||||||
|
profile_key=row_scene_profile_key(row),
|
||||||
|
)
|
||||||
if not old_composition or new_composition == old_composition:
|
if not old_composition or new_composition == old_composition:
|
||||||
return row
|
return row
|
||||||
row["source_composition"] = row.get("source_composition") or old_composition
|
row["source_composition"] = row.get("source_composition") or old_composition
|
||||||
@@ -70,6 +89,29 @@ def apply_contextual_composition(row: dict[str, Any], subject_kind: str) -> dict
|
|||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def scene_camera_profile_metadata(
|
||||||
|
scene_text: Any = "",
|
||||||
|
*,
|
||||||
|
scene_entry: Any = None,
|
||||||
|
theme: Any = "",
|
||||||
|
profile_key: Any = "",
|
||||||
|
) -> dict[str, str]:
|
||||||
|
profile = scene_camera_adapters.scene_camera_profile(
|
||||||
|
scene_text,
|
||||||
|
scene_entry=scene_entry,
|
||||||
|
theme=theme,
|
||||||
|
profile_key=profile_key,
|
||||||
|
)
|
||||||
|
if not profile:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"key": str(profile.get("key") or ""),
|
||||||
|
"family": str(profile.get("family") or ""),
|
||||||
|
"layout_label": str(profile.get("layout_label") or ""),
|
||||||
|
"place": str(profile.get("place") or ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def camera_scene_directive_for_context(
|
def camera_scene_directive_for_context(
|
||||||
scene_text: Any,
|
scene_text: Any,
|
||||||
composition: Any,
|
composition: Any,
|
||||||
@@ -77,6 +119,10 @@ def camera_scene_directive_for_context(
|
|||||||
pov_labels: list[str] | None = None,
|
pov_labels: list[str] | None = None,
|
||||||
subject_kind: str = "subjects",
|
subject_kind: str = "subjects",
|
||||||
compact_labels: Mapping[str, str] | None = None,
|
compact_labels: Mapping[str, str] | None = None,
|
||||||
|
*,
|
||||||
|
scene_entry: Any = None,
|
||||||
|
theme: Any = "",
|
||||||
|
profile_key: Any = "",
|
||||||
) -> tuple[str, dict[str, Any]]:
|
) -> tuple[str, dict[str, Any]]:
|
||||||
parsed = camera_policy.parse_camera_config(camera_config)
|
parsed = camera_policy.parse_camera_config(camera_config)
|
||||||
directive = scene_camera_adapters.camera_scene_directive_for_context(
|
directive = scene_camera_adapters.camera_scene_directive_for_context(
|
||||||
@@ -85,6 +131,9 @@ def camera_scene_directive_for_context(
|
|||||||
pov_labels,
|
pov_labels,
|
||||||
subject_kind,
|
subject_kind,
|
||||||
compact_labels,
|
compact_labels,
|
||||||
|
scene_entry=scene_entry,
|
||||||
|
theme=theme,
|
||||||
|
profile_key=profile_key,
|
||||||
)
|
)
|
||||||
return directive, parsed
|
return directive, parsed
|
||||||
|
|
||||||
@@ -129,13 +178,25 @@ def apply_camera_config(
|
|||||||
pov_labels = row_pov_labels(row, pov_label_resolver)
|
pov_labels = row_pov_labels(row, pov_label_resolver)
|
||||||
subject_kind = row_camera_subject_kind(row)
|
subject_kind = row_camera_subject_kind(row)
|
||||||
row = apply_contextual_composition(row, subject_kind)
|
row = apply_contextual_composition(row, subject_kind)
|
||||||
|
profile_metadata = scene_camera_profile_metadata(
|
||||||
|
row_scene_text(row),
|
||||||
|
scene_entry=row.get("scene_entry"),
|
||||||
|
theme=row_scene_theme(row),
|
||||||
|
profile_key=row_scene_profile_key(row),
|
||||||
|
)
|
||||||
|
if profile_metadata:
|
||||||
|
row["scene_camera_profile"] = profile_metadata
|
||||||
|
row["scene_camera_profile_key"] = profile_metadata.get("key", "")
|
||||||
scene_directive, parsed = camera_scene_directive_for_context(
|
scene_directive, parsed = camera_scene_directive_for_context(
|
||||||
row.get("scene_text") or row.get("source_scene_text") or row.get("scene"),
|
row_scene_text(row),
|
||||||
row.get("composition") or row.get("source_composition"),
|
row.get("composition") or row.get("source_composition"),
|
||||||
parsed,
|
parsed,
|
||||||
pov_labels,
|
pov_labels,
|
||||||
subject_kind,
|
subject_kind,
|
||||||
compact_labels,
|
compact_labels,
|
||||||
|
scene_entry=row.get("scene_entry"),
|
||||||
|
theme=row_scene_theme(row),
|
||||||
|
profile_key=row_scene_profile_key(row),
|
||||||
)
|
)
|
||||||
row["camera_config"] = parsed
|
row["camera_config"] = parsed
|
||||||
row["camera_scene_directive"] = scene_directive
|
row["camera_scene_directive"] = scene_directive
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -45,7 +46,8 @@ def is_pose_content_category(category: dict[str, Any], subcategory: dict[str, An
|
|||||||
subcategory.get("item_label", ""),
|
subcategory.get("item_label", ""),
|
||||||
)
|
)
|
||||||
).lower()
|
).lower()
|
||||||
return "pose" in haystack or "sex" in haystack
|
tokens = set(re.findall(r"[a-z0-9]+", haystack))
|
||||||
|
return bool(tokens.intersection({"pose", "poses", "sex", "sexual"}))
|
||||||
|
|
||||||
|
|
||||||
def cast_count_adjustment(
|
def cast_count_adjustment(
|
||||||
|
|||||||
+142
-20
@@ -8,10 +8,12 @@ try:
|
|||||||
from . import category_library as category_policy
|
from . import category_library as category_policy
|
||||||
from . import category_template_metadata as template_policy
|
from . import category_template_metadata as template_policy
|
||||||
from . import generate_prompt_batches as g
|
from . import generate_prompt_batches as g
|
||||||
|
from . import outercourse_action_policy as outercourse_policy
|
||||||
except ImportError: # Allows local smoke tests with top-level imports.
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
import category_library as category_policy
|
import category_library as category_policy
|
||||||
import category_template_metadata as template_policy
|
import category_template_metadata as template_policy
|
||||||
import generate_prompt_batches as g
|
import generate_prompt_batches as g
|
||||||
|
import outercourse_action_policy as outercourse_policy
|
||||||
|
|
||||||
|
|
||||||
class SafeFormatDict(dict):
|
class SafeFormatDict(dict):
|
||||||
@@ -182,8 +184,8 @@ def oral_axis_values_for_context(values: list[Any], position: str, oral_act: str
|
|||||||
|
|
||||||
|
|
||||||
def outercourse_acts_for_position(values: list[Any], position: str) -> list[Any]:
|
def outercourse_acts_for_position(values: list[Any], position: str) -> list[Any]:
|
||||||
position_text = str(position or "").lower()
|
action_kind = outercourse_policy.infer_outercourse_action_kind(position)
|
||||||
if not position_text:
|
if action_kind == outercourse_policy.OUTERCOURSE_GENERIC:
|
||||||
return values
|
return values
|
||||||
|
|
||||||
def act_text(value: Any) -> str:
|
def act_text(value: Any) -> str:
|
||||||
@@ -193,22 +195,22 @@ def outercourse_acts_for_position(values: list[Any], position: str) -> list[Any]
|
|||||||
matches = [value for value in values if predicate(act_text(value))]
|
matches = [value for value in values if predicate(act_text(value))]
|
||||||
return matches or values
|
return matches or values
|
||||||
|
|
||||||
if any(term in position_text for term in ("boobjob", "titjob", "breast-sex", "breast sex")):
|
if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
|
||||||
return filtered(lambda text: any(term in text for term in ("boobjob", "titjob", "breast sex", "breasts")))
|
return filtered(lambda text: any(term in text for term in ("boobjob", "titjob", "breast sex", "breasts")))
|
||||||
if any(term in position_text for term in ("testicle", "balls")):
|
if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
|
||||||
return filtered(lambda text: any(term in text for term in ("testicle", "balls")))
|
return filtered(lambda text: any(term in text for term in ("testicle", "balls")))
|
||||||
if "penis-licking" in position_text or "penis licking" in position_text:
|
if action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING:
|
||||||
return filtered(lambda text: "licking" in text or "tongue" in text)
|
return filtered(lambda text: "licking" in text or "tongue" in text)
|
||||||
if "handjob" in position_text or "hand job" in position_text:
|
if action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
|
||||||
return filtered(lambda text: any(term in text for term in ("handjob", "hand job", "hand wrapped", "two-handed")))
|
return filtered(lambda text: any(term in text for term in ("handjob", "hand job", "hand wrapped", "two-handed")))
|
||||||
if "footjob" in position_text:
|
if action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
|
||||||
return filtered(lambda text: any(term in text for term in ("footjob", "feet", "soles", "toes")))
|
return filtered(lambda text: any(term in text for term in ("footjob", "feet", "soles", "toes")))
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
def outercourse_axis_values_for_position(values: list[Any], position: str, axis_name: str) -> list[Any]:
|
def outercourse_axis_values_for_position(values: list[Any], position: str, axis_name: str) -> list[Any]:
|
||||||
position_text = str(position or "").lower()
|
action_kind = outercourse_policy.infer_outercourse_action_kind(position)
|
||||||
if not position_text:
|
if action_kind == outercourse_policy.OUTERCOURSE_GENERIC:
|
||||||
return values
|
return values
|
||||||
axis_name = str(axis_name or "").lower()
|
axis_name = str(axis_name or "").lower()
|
||||||
if axis_name not in {"contact_detail", "hand_detail", "texture_detail", "visibility", "body_contact"}:
|
if axis_name not in {"contact_detail", "hand_detail", "texture_detail", "visibility", "body_contact"}:
|
||||||
@@ -224,15 +226,25 @@ def outercourse_axis_values_for_position(values: list[Any], position: str, axis_
|
|||||||
if any(term in value_text(value) for term in terms)
|
if any(term in value_text(value) for term in terms)
|
||||||
and not any(term in value_text(value) for term in excluded_terms)
|
and not any(term in value_text(value) for term in excluded_terms)
|
||||||
]
|
]
|
||||||
return matches or values
|
if matches:
|
||||||
|
return matches
|
||||||
|
if excluded_terms:
|
||||||
|
non_excluded = [
|
||||||
|
value
|
||||||
|
for value in values
|
||||||
|
if not any(term in value_text(value) for term in excluded_terms)
|
||||||
|
]
|
||||||
|
if non_excluded:
|
||||||
|
return non_excluded
|
||||||
|
return values
|
||||||
|
|
||||||
if any(term in position_text for term in ("boobjob", "titjob", "breast-sex", "breast sex")):
|
if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
|
||||||
by_axis = {
|
by_axis = {
|
||||||
"contact_detail": ("compressed", "glans", "breast", "breasts", "soft tissue", "skin visibly"),
|
"contact_detail": ("compressed", "glans", "breast", "breasts", "soft tissue", "skin visibly"),
|
||||||
"hand_detail": ("breast", "breasts", "fingers"),
|
"hand_detail": ("breast", "breasts", "fingers"),
|
||||||
"texture_detail": ("compression", "soft flesh", "skin", "flesh", "asymmetry"),
|
"texture_detail": ("compression", "soft flesh", "skin", "flesh", "asymmetry"),
|
||||||
"visibility": ("breast", "breasts", "glans", "shaft"),
|
"visibility": ("breast", "breasts", "glans", "shaft"),
|
||||||
"body_contact": ("torso", "body angled", "shoulders", "hips"),
|
"body_contact": ("torso", "body angle", "body angled", "shoulders", "hips"),
|
||||||
}
|
}
|
||||||
excluded_by_axis = {
|
excluded_by_axis = {
|
||||||
"contact_detail": ("hand wrapped", "fingers and palm", "soles", "toes", "balls", "tongue"),
|
"contact_detail": ("hand wrapped", "fingers and palm", "soles", "toes", "balls", "tongue"),
|
||||||
@@ -245,7 +257,7 @@ def outercourse_axis_values_for_position(values: list[Any], position: str, axis_
|
|||||||
by_axis.get(axis_name, ("breast", "breasts", "shaft")),
|
by_axis.get(axis_name, ("breast", "breasts", "shaft")),
|
||||||
excluded_by_axis.get(axis_name, ()),
|
excluded_by_axis.get(axis_name, ()),
|
||||||
)
|
)
|
||||||
if any(term in position_text for term in ("testicle", "balls")):
|
if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
|
||||||
by_axis = {
|
by_axis = {
|
||||||
"contact_detail": ("balls", "lips", "tongue", "wet"),
|
"contact_detail": ("balls", "lips", "tongue", "wet"),
|
||||||
"hand_detail": ("balls", "base", "thigh"),
|
"hand_detail": ("balls", "base", "thigh"),
|
||||||
@@ -254,7 +266,7 @@ def outercourse_axis_values_for_position(values: list[Any], position: str, axis_
|
|||||||
"body_contact": ("torso", "shoulders", "head tucked", "base of the penis", "knees", "thigh"),
|
"body_contact": ("torso", "shoulders", "head tucked", "base of the penis", "knees", "thigh"),
|
||||||
}
|
}
|
||||||
return filtered(by_axis.get(axis_name, ("balls", "mouth", "tongue")))
|
return filtered(by_axis.get(axis_name, ("balls", "mouth", "tongue")))
|
||||||
if "penis-licking" in position_text or "penis licking" in position_text:
|
if action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING:
|
||||||
by_axis = {
|
by_axis = {
|
||||||
"contact_detail": ("tongue", "lips", "glans", "shaft", "wet"),
|
"contact_detail": ("tongue", "lips", "glans", "shaft", "wet"),
|
||||||
"hand_detail": ("base", "penis", "thigh"),
|
"hand_detail": ("base", "penis", "thigh"),
|
||||||
@@ -263,7 +275,7 @@ def outercourse_axis_values_for_position(values: list[Any], position: str, axis_
|
|||||||
"body_contact": ("head low", "face directly", "torso", "pelvis", "base of the penis", "hips", "body angled"),
|
"body_contact": ("head low", "face directly", "torso", "pelvis", "base of the penis", "hips", "body angled"),
|
||||||
}
|
}
|
||||||
return filtered(by_axis.get(axis_name, ("tongue", "glans", "shaft")))
|
return filtered(by_axis.get(axis_name, ("tongue", "glans", "shaft")))
|
||||||
if "handjob" in position_text or "hand job" in position_text:
|
if action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
|
||||||
by_axis = {
|
by_axis = {
|
||||||
"contact_detail": ("hand", "fingers", "palm", "shaft", "glans"),
|
"contact_detail": ("hand", "fingers", "palm", "shaft", "glans"),
|
||||||
"hand_detail": ("hand", "hands", "shaft", "penis"),
|
"hand_detail": ("hand", "hands", "shaft", "penis"),
|
||||||
@@ -271,8 +283,17 @@ def outercourse_axis_values_for_position(values: list[Any], position: str, axis_
|
|||||||
"visibility": ("hand", "penis", "shaft", "glans"),
|
"visibility": ("hand", "penis", "shaft", "glans"),
|
||||||
"body_contact": ("hips", "knees", "body angle"),
|
"body_contact": ("hips", "knees", "body angle"),
|
||||||
}
|
}
|
||||||
return filtered(by_axis.get(axis_name, ("hand", "penis", "shaft")))
|
excluded_by_axis = {
|
||||||
if "footjob" in position_text:
|
"contact_detail": ("balls", "soles", "toes", "breast", "breasts", "tongue"),
|
||||||
|
"hand_detail": ("balls", "thigh", "ankles", "breast", "breasts"),
|
||||||
|
"texture_detail": ("toes", "soles", "tongue", "breast", "breasts"),
|
||||||
|
"visibility": ("balls", "feet", "soles", "breast", "mouth"),
|
||||||
|
}
|
||||||
|
return filtered(
|
||||||
|
by_axis.get(axis_name, ("hand", "penis", "shaft")),
|
||||||
|
excluded_by_axis.get(axis_name, ()),
|
||||||
|
)
|
||||||
|
if action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
|
||||||
by_axis = {
|
by_axis = {
|
||||||
"contact_detail": ("soles", "toes"),
|
"contact_detail": ("soles", "toes"),
|
||||||
"hand_detail": ("ankles", "thighs"),
|
"hand_detail": ("ankles", "thighs"),
|
||||||
@@ -292,6 +313,96 @@ def outercourse_axis_values_for_position(values: list[Any], position: str, axis_
|
|||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def anal_axis_values_for_position(values: list[Any], position: str, axis_name: str) -> list[Any]:
|
||||||
|
position_text = str(position or "").lower()
|
||||||
|
if not position_text:
|
||||||
|
return values
|
||||||
|
axis_name = str(axis_name or "").lower()
|
||||||
|
if axis_name not in {"body_contact", "hand_detail", "leg_detail", "thrust_detail", "visibility"}:
|
||||||
|
return values
|
||||||
|
|
||||||
|
def value_text(value: Any) -> str:
|
||||||
|
return entry_text(value).lower()
|
||||||
|
|
||||||
|
def filtered(terms: tuple[str, ...], excluded_terms: tuple[str, ...] = ()) -> list[Any]:
|
||||||
|
matches = [
|
||||||
|
value
|
||||||
|
for value in values
|
||||||
|
if any(term in value_text(value) for term in terms)
|
||||||
|
and not any(term in value_text(value) for term in excluded_terms)
|
||||||
|
]
|
||||||
|
if matches:
|
||||||
|
return matches
|
||||||
|
if excluded_terms:
|
||||||
|
non_excluded = [
|
||||||
|
value
|
||||||
|
for value in values
|
||||||
|
if not any(term in value_text(value) for term in excluded_terms)
|
||||||
|
]
|
||||||
|
if non_excluded:
|
||||||
|
return non_excluded
|
||||||
|
return values
|
||||||
|
|
||||||
|
if "side-lying" in position_text or "spooning" in position_text:
|
||||||
|
by_axis = {
|
||||||
|
"body_contact": ("bodies locked", "chests pressed", "sweaty", "hips pressed"),
|
||||||
|
"hand_detail": ("hips", "waist", "cheeks", "shoulders"),
|
||||||
|
"leg_detail": ("one leg lifted", "thighs held open", "legs spread"),
|
||||||
|
"thrust_detail": ("pelvis pressed", "bodies rocking", "wet skin", "hard grinding"),
|
||||||
|
"visibility": ("ass and penis", "anal penetration", "spread cheeks", "genital contact"),
|
||||||
|
}
|
||||||
|
return filtered(
|
||||||
|
by_axis.get(axis_name, ("side", "thigh", "hips")),
|
||||||
|
("standing", "kneeling", "draped over shoulders", "knees pressed to chest"),
|
||||||
|
)
|
||||||
|
if "standing" in position_text:
|
||||||
|
by_axis = {
|
||||||
|
"body_contact": ("hips pressed", "bodies locked", "one body bent over", "ass lifted", "sweaty"),
|
||||||
|
"hand_detail": ("hips", "waist", "cheeks", "shoulders"),
|
||||||
|
"leg_detail": ("standing", "one foot planted"),
|
||||||
|
"thrust_detail": ("hips", "pelvis", "hard grinding", "bodies rocking"),
|
||||||
|
"visibility": ("ass and penis", "anal penetration", "spread cheeks", "genital contact"),
|
||||||
|
}
|
||||||
|
return filtered(
|
||||||
|
by_axis.get(axis_name, ("standing", "hips")),
|
||||||
|
("kneeling", "draped over shoulders", "knees pressed to chest", "side-lying"),
|
||||||
|
)
|
||||||
|
if "edge-of-bed" in position_text or "bed-edge" in position_text or "edge supported" in position_text:
|
||||||
|
by_axis = {
|
||||||
|
"body_contact": ("thighs held open", "hips pressed", "bodies locked", "ass lifted"),
|
||||||
|
"hand_detail": ("hips", "waist", "cheeks", "thighs"),
|
||||||
|
"leg_detail": ("knees pressed", "legs draped", "thighs held open", "one foot planted"),
|
||||||
|
"thrust_detail": ("hips", "pelvis", "hard grinding", "bodies rocking"),
|
||||||
|
"visibility": ("ass and penis", "anal penetration", "open thighs", "genital contact"),
|
||||||
|
}
|
||||||
|
return filtered(by_axis.get(axis_name, ("thigh", "hips")), ("standing", "side-lying"))
|
||||||
|
if "kneeling" in position_text:
|
||||||
|
by_axis = {
|
||||||
|
"body_contact": ("ass lifted", "hips pressed", "bodies locked", "one body bent over"),
|
||||||
|
"hand_detail": ("hips", "waist", "cheeks", "thighs"),
|
||||||
|
"leg_detail": ("kneeling", "thighs held open", "legs spread"),
|
||||||
|
"thrust_detail": ("hips", "pelvis", "ass pushed", "hard grinding"),
|
||||||
|
"visibility": ("ass and penis", "anal penetration", "spread cheeks", "genital contact"),
|
||||||
|
}
|
||||||
|
return filtered(
|
||||||
|
by_axis.get(axis_name, ("kneeling", "hips")),
|
||||||
|
("standing", "draped over shoulders", "knees pressed to chest", "side-lying"),
|
||||||
|
)
|
||||||
|
if "doggy" in position_text or "face-down" in position_text or "bent-over" in position_text:
|
||||||
|
by_axis = {
|
||||||
|
"body_contact": ("ass lifted", "one body bent over", "hips pressed", "bodies locked"),
|
||||||
|
"hand_detail": ("hips", "waist", "cheeks", "thighs"),
|
||||||
|
"leg_detail": ("legs spread", "kneeling", "one foot planted", "standing"),
|
||||||
|
"thrust_detail": ("ass pushed", "hips", "pelvis", "hard grinding"),
|
||||||
|
"visibility": ("ass and penis", "anal penetration", "spread cheeks", "genital contact"),
|
||||||
|
}
|
||||||
|
excluded = ("side-lying", "draped over shoulders", "knees pressed to chest")
|
||||||
|
if "face-down" in position_text or "doggy" in position_text:
|
||||||
|
excluded = (*excluded, "standing")
|
||||||
|
return filtered(by_axis.get(axis_name, ("ass", "hips")), excluded)
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
def _format(template: str, context: dict[str, Any]) -> str:
|
def _format(template: str, context: dict[str, Any]) -> str:
|
||||||
fields = {key for _, key, _, _ in Formatter().parse(template) if key}
|
fields = {key for _, key, _, _ in Formatter().parse(template) if key}
|
||||||
safe_context = SafeFormatDict({key: "" for key in fields})
|
safe_context = SafeFormatDict({key: "" for key in fields})
|
||||||
@@ -309,6 +420,7 @@ def compose_item(
|
|||||||
) -> tuple[str, str, dict[str, str], dict[str, Any]]:
|
) -> tuple[str, str, dict[str, str], dict[str, Any]]:
|
||||||
templates = category_policy.template_list(category, subcategory, item, "item_templates")
|
templates = category_policy.template_list(category, subcategory, item, "item_templates")
|
||||||
axes = category_policy.merged_axes(category, subcategory, item)
|
axes = category_policy.merged_axes(category, subcategory, item)
|
||||||
|
inherited_metadata = template_policy.inherited_template_metadata(category, subcategory, item)
|
||||||
if templates and axes:
|
if templates and axes:
|
||||||
template_entry = weighted_choice(rng, category_policy.compatible_entries(templates, women_count, men_count))
|
template_entry = weighted_choice(rng, category_policy.compatible_entries(templates, women_count, men_count))
|
||||||
template = entry_text(template_entry)
|
template = entry_text(template_entry)
|
||||||
@@ -316,7 +428,7 @@ def compose_item(
|
|||||||
unique_fields = list(dict.fromkeys(fields))
|
unique_fields = list(dict.fromkeys(fields))
|
||||||
axis_values: dict[str, str] = {}
|
axis_values: dict[str, str] = {}
|
||||||
subcategory_slug = str(subcategory.get("slug") or "").lower()
|
subcategory_slug = str(subcategory.get("slug") or "").lower()
|
||||||
if subcategory_slug in ("oral_sex", "outercourse_sex") and "position" in unique_fields and axes.get("position"):
|
if subcategory_slug in ("oral_sex", "outercourse_sex", "anal_double_penetration") and "position" in unique_fields and axes.get("position"):
|
||||||
position_values = category_policy.compatible_entries(axes["position"], women_count, men_count)
|
position_values = category_policy.compatible_entries(axes["position"], women_count, men_count)
|
||||||
axis_values["position"] = entry_text(weighted_choice(rng, position_values))
|
axis_values["position"] = entry_text(weighted_choice(rng, position_values))
|
||||||
for name in unique_fields:
|
for name in unique_fields:
|
||||||
@@ -336,8 +448,18 @@ def compose_item(
|
|||||||
values = outercourse_acts_for_position(values, axis_values.get("position", ""))
|
values = outercourse_acts_for_position(values, axis_values.get("position", ""))
|
||||||
if subcategory_slug == "outercourse_sex":
|
if subcategory_slug == "outercourse_sex":
|
||||||
values = outercourse_axis_values_for_position(values, axis_values.get("position", ""), name)
|
values = outercourse_axis_values_for_position(values, axis_values.get("position", ""), name)
|
||||||
|
if subcategory_slug == "anal_double_penetration":
|
||||||
|
values = anal_axis_values_for_position(values, axis_values.get("position", ""), name)
|
||||||
axis_values[name] = entry_text(weighted_choice(rng, values))
|
axis_values[name] = entry_text(weighted_choice(rng, values))
|
||||||
item_prompt = _format(template, axis_values).strip()
|
item_prompt = _format(template, axis_values).strip()
|
||||||
name = item_name(item) or subcategory["name"]
|
name = item_name(item) or subcategory["name"]
|
||||||
return item_prompt, name, axis_values, template_policy.template_metadata(template_entry)
|
return (
|
||||||
return item_text(item), item_name(item), {}, template_policy.template_metadata(item)
|
item_prompt,
|
||||||
|
name,
|
||||||
|
axis_values,
|
||||||
|
template_policy.merge_template_metadata(inherited_metadata, template_policy.template_metadata(template_entry)),
|
||||||
|
)
|
||||||
|
return item_text(item), item_name(item), {}, template_policy.merge_template_metadata(
|
||||||
|
inherited_metadata,
|
||||||
|
template_policy.template_metadata(item),
|
||||||
|
)
|
||||||
|
|||||||
+38
-2
@@ -89,8 +89,31 @@ def _choose_pair(rng: random.Random, items: list[Any]) -> tuple[str, str]:
|
|||||||
return _pair_from(_weighted_choice(rng, items))
|
return _pair_from(_weighted_choice(rng, items))
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_entry(value: Any, *, slug: str = "", text: str = "") -> dict[str, Any]:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
entry = dict(value)
|
||||||
|
elif isinstance(value, (list, tuple)) and len(value) == 2:
|
||||||
|
entry = {"slug": str(value[0]), "prompt": str(value[1])}
|
||||||
|
else:
|
||||||
|
entry = {"prompt": str(value or "")}
|
||||||
|
if slug:
|
||||||
|
entry["slug"] = slug
|
||||||
|
if text:
|
||||||
|
if "prompt" in entry:
|
||||||
|
entry["prompt"] = text
|
||||||
|
elif "text" in entry:
|
||||||
|
entry["text"] = text
|
||||||
|
else:
|
||||||
|
entry["prompt"] = text
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
def _choose_text(rng: random.Random, items: list[Any]) -> str:
|
def _choose_text(rng: random.Random, items: list[Any]) -> str:
|
||||||
item = _weighted_choice(rng, items)
|
item = _weighted_choice(rng, items)
|
||||||
|
return _text_from_entry(item)
|
||||||
|
|
||||||
|
|
||||||
|
def _text_from_entry(item: Any) -> str:
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
return str(
|
return str(
|
||||||
item.get("template")
|
item.get("template")
|
||||||
@@ -134,13 +157,22 @@ def apply_location_config_to_legacy_row(
|
|||||||
else:
|
else:
|
||||||
choices = location_entries
|
choices = location_entries
|
||||||
scene_rng = seed_policy.axis_rng(seed_config, "scene", seed, row_number)
|
scene_rng = seed_policy.axis_rng(seed_config, "scene", seed, row_number)
|
||||||
scene_slug, scene_text = _choose_pair(scene_rng, choices)
|
scene_choice = _weighted_choice(scene_rng, choices)
|
||||||
|
scene_slug, scene_text = _pair_from(scene_choice)
|
||||||
|
scene_entry = _metadata_entry(scene_choice, slug=scene_slug, text=scene_text)
|
||||||
old_slug = str(row.get("scene") or "")
|
old_slug = str(row.get("scene") or "")
|
||||||
old_text = legacy_scene_text_for_slug(old_slug)
|
old_text = legacy_scene_text_for_slug(old_slug)
|
||||||
row["source_scene"] = old_slug
|
row["source_scene"] = old_slug
|
||||||
row["source_scene_text"] = old_text
|
row["source_scene_text"] = old_text
|
||||||
row["scene"] = scene_slug
|
row["scene"] = scene_slug
|
||||||
row["scene_text"] = scene_text
|
row["scene_text"] = scene_text
|
||||||
|
row["scene_entry"] = scene_entry
|
||||||
|
row["location_theme"] = str(location_config.get("theme") or "")
|
||||||
|
row["scene_theme"] = scene_entry.get("theme", "") or (
|
||||||
|
str(location_config.get("theme") or "")
|
||||||
|
if location_config.get("apply_mode") == "replace"
|
||||||
|
else ""
|
||||||
|
)
|
||||||
row["location_config"] = location_config
|
row["location_config"] = location_config
|
||||||
if old_text:
|
if old_text:
|
||||||
row["prompt"] = str(row.get("prompt") or "").replace(f"Scene: {old_text}.", f"Scene: {scene_text}.")
|
row["prompt"] = str(row.get("prompt") or "").replace(f"Scene: {old_text}.", f"Scene: {scene_text}.")
|
||||||
@@ -178,12 +210,16 @@ def apply_composition_config_to_legacy_row(
|
|||||||
else:
|
else:
|
||||||
choices = composition_entries
|
choices = composition_entries
|
||||||
composition_rng = seed_policy.axis_rng(seed_config, "composition", seed, row_number)
|
composition_rng = seed_policy.axis_rng(seed_config, "composition", seed, row_number)
|
||||||
new_composition = _choose_text(composition_rng, choices)
|
composition_choice = _weighted_choice(composition_rng, choices)
|
||||||
|
new_composition = _text_from_entry(composition_choice)
|
||||||
|
composition_entry = _metadata_entry(composition_choice, text=new_composition)
|
||||||
old_composition = str(row.get("composition") or "")
|
old_composition = str(row.get("composition") or "")
|
||||||
old_prompt_fragment = f"Composition: vertical {old_composition}."
|
old_prompt_fragment = f"Composition: vertical {old_composition}."
|
||||||
new_prompt_fragment = f"Composition: {row_camera.composition_prompt(new_composition)}."
|
new_prompt_fragment = f"Composition: {row_camera.composition_prompt(new_composition)}."
|
||||||
row["source_composition"] = old_composition
|
row["source_composition"] = old_composition
|
||||||
row["composition"] = new_composition
|
row["composition"] = new_composition
|
||||||
|
row["composition_entry"] = composition_entry
|
||||||
|
row["composition_theme"] = str(composition_config.get("theme") or "")
|
||||||
row["composition_prompt"] = row_camera.composition_prompt(new_composition)
|
row["composition_prompt"] = row_camera.composition_prompt(new_composition)
|
||||||
row["composition_config"] = composition_config
|
row["composition_config"] = composition_config
|
||||||
if old_composition:
|
if old_composition:
|
||||||
|
|||||||
+240
-16
@@ -1,11 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .prompt_hygiene import sanitize_caption_text, sanitize_negative_text, sanitize_prompt_text
|
from . import generate_prompt_batches as prompt_batches
|
||||||
|
from . import row_location as row_location_policy
|
||||||
|
from .prompt_hygiene import combine_negative_text, sanitize_caption_text, sanitize_negative_text, sanitize_prompt_text
|
||||||
except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.py`.
|
except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.py`.
|
||||||
from prompt_hygiene import sanitize_caption_text, sanitize_negative_text, sanitize_prompt_text
|
import generate_prompt_batches as prompt_batches
|
||||||
|
import row_location as row_location_policy
|
||||||
|
from prompt_hygiene import combine_negative_text, sanitize_caption_text, sanitize_negative_text, sanitize_prompt_text
|
||||||
|
|
||||||
|
|
||||||
def _trigger_tuple(active_trigger: str) -> tuple[str, ...]:
|
def _trigger_tuple(active_trigger: str) -> tuple[str, ...]:
|
||||||
@@ -24,8 +29,7 @@ def prepend_trigger(prompt: str, trigger: str, enabled: bool) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def combined_negative(base: str, extra: str) -> str:
|
def combined_negative(base: str, extra: str) -> str:
|
||||||
parts = [str(part).strip() for part in (base, extra) if part and str(part).strip()]
|
return combine_negative_text(base, extra)
|
||||||
return ", ".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def caption_from_parts(parts: list[Any] | tuple[Any, ...], *, active_trigger: str = "") -> str:
|
def caption_from_parts(parts: list[Any] | tuple[Any, ...], *, active_trigger: str = "") -> str:
|
||||||
@@ -33,6 +37,180 @@ def caption_from_parts(parts: list[Any] | tuple[Any, ...], *, active_trigger: st
|
|||||||
return sanitize_caption_text(text, triggers=_trigger_tuple(active_trigger))
|
return sanitize_caption_text(text, triggers=_trigger_tuple(active_trigger))
|
||||||
|
|
||||||
|
|
||||||
|
def _setdefault_nonempty(row: dict[str, Any], key: str, value: Any) -> None:
|
||||||
|
if str(row.get(key) or "").strip():
|
||||||
|
return
|
||||||
|
if str(value or "").strip():
|
||||||
|
row[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def _setdefault_count(row: dict[str, Any], key: str, value: int) -> None:
|
||||||
|
if str(row.get(key) or "").strip():
|
||||||
|
return
|
||||||
|
row[key] = int(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_subject_metadata(row: dict[str, Any]) -> tuple[str, str, int | None, int | None]:
|
||||||
|
subject = str(row.get("primary_subject") or row.get("subject") or "").strip()
|
||||||
|
lower = subject.lower()
|
||||||
|
if lower in ("woman", "adult woman"):
|
||||||
|
return "woman", subject or "woman", 1, 0
|
||||||
|
if lower in ("man", "adult man"):
|
||||||
|
return "man", subject or "man", 0, 1
|
||||||
|
if "two women" in lower:
|
||||||
|
return "couple", subject or "two women", 2, 0
|
||||||
|
if "two men" in lower:
|
||||||
|
return "couple", subject or "two men", 0, 2
|
||||||
|
if "woman" in lower and "man" in lower:
|
||||||
|
return "couple", subject or "a woman and a man", 1, 1
|
||||||
|
if "group" in lower:
|
||||||
|
return "group", subject or "mixed adult group", 2, 2
|
||||||
|
if "layout" in lower:
|
||||||
|
return "layout", subject or "adult layout scene", None, None
|
||||||
|
return "", subject, None, None
|
||||||
|
|
||||||
|
|
||||||
|
_LEGACY_PROMPT_FIELD_LABELS = (
|
||||||
|
"Ages",
|
||||||
|
"Body types",
|
||||||
|
"Scene",
|
||||||
|
"Pose",
|
||||||
|
"Facial expressions",
|
||||||
|
"Facial expression",
|
||||||
|
"Clothing",
|
||||||
|
"Prop/detail",
|
||||||
|
"Composition",
|
||||||
|
"Use",
|
||||||
|
"Avoid",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_text(value: Any) -> str:
|
||||||
|
text = "" if value is None else str(value)
|
||||||
|
text = re.sub(r"\s+", " ", text.replace("\n", " ")).strip()
|
||||||
|
return re.sub(r"\s+([,.;:])", r"\1", text)
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_prompt_field(row: dict[str, Any], label: str) -> str:
|
||||||
|
prompt = _clean_text(row.get("prompt"))
|
||||||
|
if not prompt:
|
||||||
|
return ""
|
||||||
|
labels = "|".join(re.escape(name) for name in _LEGACY_PROMPT_FIELD_LABELS)
|
||||||
|
pattern = rf"{re.escape(label)}:\s*(.*?)(?=\. (?:{labels}):|\. Use\b|\. Avoid\b|$)"
|
||||||
|
match = re.search(pattern, prompt)
|
||||||
|
if not match:
|
||||||
|
return ""
|
||||||
|
return _clean_text(match.group(1)).rstrip(".")
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_legacy_pose(value: Any) -> str:
|
||||||
|
text = _clean_text(value)
|
||||||
|
text = text.replace(", affectionate and flirtatious but non-explicit", "")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_legacy_clothing(value: Any) -> str:
|
||||||
|
text = _clean_text(value)
|
||||||
|
text = re.sub(r",?\s*(?:fashion editorial|resort) styling$", "", text, flags=re.IGNORECASE)
|
||||||
|
return text.strip(" ,")
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_body_phrase(row: dict[str, Any]) -> str:
|
||||||
|
body_phrase = _clean_text(row.get("body_phrase"))
|
||||||
|
if body_phrase:
|
||||||
|
return body_phrase
|
||||||
|
body = _clean_text(row.get("body_type") or row.get("body"))
|
||||||
|
if not body:
|
||||||
|
return ""
|
||||||
|
figure_note = _clean_text(row.get("figure") or row.get("figure_note"))
|
||||||
|
return _clean_text(prompt_batches.make_body_phrase(body, figure_note))
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_legacy_caption_lead(caption: str) -> str:
|
||||||
|
pieces = caption.split(", ", 1)
|
||||||
|
if len(pieces) == 2 and pieces[0].strip().lower() not in ("woman", "man"):
|
||||||
|
return pieces[1].strip()
|
||||||
|
return caption
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_single_caption_front(row: dict[str, Any]) -> dict[str, str]:
|
||||||
|
caption = _strip_legacy_caption_lead(_clean_text(row.get("caption")))
|
||||||
|
if not caption:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
subject = _clean_text(row.get("primary_subject") or row.get("subject"))
|
||||||
|
age = _clean_text(row.get("age_band") or row.get("age"))
|
||||||
|
body_phrase = _legacy_body_phrase(row)
|
||||||
|
if subject.lower() in ("woman", "man") and age and body_phrase:
|
||||||
|
prefix = f"{subject}, {age}, {body_phrase}, "
|
||||||
|
if caption.lower().startswith(prefix.lower()):
|
||||||
|
try:
|
||||||
|
skin, hair, eyes, _rest = caption[len(prefix) :].split(", ", 3)
|
||||||
|
except ValueError:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"caption_subject": subject,
|
||||||
|
"caption_age": age,
|
||||||
|
"caption_body_phrase": body_phrase,
|
||||||
|
"caption_skin": skin,
|
||||||
|
"caption_hair": hair,
|
||||||
|
"caption_eyes": eyes,
|
||||||
|
}
|
||||||
|
|
||||||
|
pieces = [piece.strip() for piece in caption.split(", ", 6)]
|
||||||
|
if len(pieces) < 7:
|
||||||
|
return {}
|
||||||
|
subject, age, body_phrase, skin, hair, eyes, _rest = pieces
|
||||||
|
if subject.lower() not in ("woman", "man"):
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"caption_subject": subject,
|
||||||
|
"caption_age": age,
|
||||||
|
"caption_body_phrase": body_phrase,
|
||||||
|
"caption_skin": skin,
|
||||||
|
"caption_hair": hair,
|
||||||
|
"caption_eyes": eyes,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def enrich_legacy_row_metadata(row: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
if row.get("source") != "built_in_generator":
|
||||||
|
return row
|
||||||
|
subject_type, subject_phrase, women_count, men_count = _legacy_subject_metadata(row)
|
||||||
|
_setdefault_nonempty(row, "subject_type", subject_type)
|
||||||
|
_setdefault_nonempty(row, "subject_phrase", subject_phrase)
|
||||||
|
if women_count is not None:
|
||||||
|
_setdefault_count(row, "women_count", women_count)
|
||||||
|
if men_count is not None:
|
||||||
|
_setdefault_count(row, "men_count", men_count)
|
||||||
|
if women_count is not None and men_count is not None and not str(row.get("person_count") or "").strip():
|
||||||
|
row["person_count"] = int(women_count) + int(men_count)
|
||||||
|
scene_slug = str(row.get("scene") or row.get("scene_slug") or "").strip()
|
||||||
|
if scene_slug and not str(row.get("scene_slug") or "").strip():
|
||||||
|
row["scene_slug"] = scene_slug
|
||||||
|
if scene_slug and not str(row.get("scene_text") or "").strip():
|
||||||
|
scene_text = row_location_policy.legacy_scene_text_for_slug(scene_slug)
|
||||||
|
if scene_text:
|
||||||
|
row["scene_text"] = scene_text
|
||||||
|
row.setdefault("scene_entry", {"slug": scene_slug, "prompt": scene_text})
|
||||||
|
if subject_type in ("woman", "man"):
|
||||||
|
front = _legacy_single_caption_front(row)
|
||||||
|
_setdefault_nonempty(row, "body_phrase", front.get("caption_body_phrase", ""))
|
||||||
|
_setdefault_nonempty(row, "skin", front.get("caption_skin", ""))
|
||||||
|
_setdefault_nonempty(row, "hair", front.get("caption_hair", ""))
|
||||||
|
_setdefault_nonempty(row, "eyes", front.get("caption_eyes", ""))
|
||||||
|
pose = _clean_legacy_pose(_legacy_prompt_field(row, "Pose"))
|
||||||
|
_setdefault_nonempty(row, "pose", pose)
|
||||||
|
expression = _legacy_prompt_field(row, "Facial expression") or _legacy_prompt_field(row, "Facial expressions")
|
||||||
|
_setdefault_nonempty(row, "expression", expression)
|
||||||
|
clothing = _clean_legacy_clothing(_legacy_prompt_field(row, "Clothing"))
|
||||||
|
_setdefault_nonempty(row, "clothing", clothing)
|
||||||
|
_setdefault_nonempty(row, "item", clothing)
|
||||||
|
if clothing:
|
||||||
|
_setdefault_nonempty(row, "item_label", "Clothing")
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
def normalize_prompt_row(
|
def normalize_prompt_row(
|
||||||
row: dict[str, Any],
|
row: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
@@ -42,6 +220,7 @@ def normalize_prompt_row(
|
|||||||
extra_negative: str = "",
|
extra_negative: str = "",
|
||||||
default_negative: str = "",
|
default_negative: str = "",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
row = enrich_legacy_row_metadata(row)
|
||||||
trigger = str(active_trigger or "").strip()
|
trigger = str(active_trigger or "").strip()
|
||||||
positive = str(extra_positive or "").strip()
|
positive = str(extra_positive or "").strip()
|
||||||
prompt = str(row.get("prompt", "") or "")
|
prompt = str(row.get("prompt", "") or "")
|
||||||
@@ -101,21 +280,25 @@ def sanitize_metadata_row_text(row: dict[str, Any], *, active_trigger: str = "")
|
|||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_pair_root_row_field(pair: dict[str, Any], row_key: str, root_key: str, row_field: str) -> None:
|
||||||
|
row = pair.get(row_key)
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
return
|
||||||
|
if root_key in pair:
|
||||||
|
row[row_field] = pair.get(root_key)
|
||||||
|
elif row_field in row:
|
||||||
|
pair[root_key] = row.get(row_field)
|
||||||
|
|
||||||
|
|
||||||
def synchronize_pair_row_outputs(pair: dict[str, Any]) -> dict[str, Any]:
|
def synchronize_pair_row_outputs(pair: dict[str, Any]) -> dict[str, Any]:
|
||||||
mapping = (
|
mapping = (
|
||||||
("softcore_row", "softcore_prompt", "softcore_caption", "softcore_negative_prompt"),
|
("softcore_row", "softcore_prompt", "softcore_caption", "softcore_negative_prompt"),
|
||||||
("hardcore_row", "hardcore_prompt", "hardcore_caption", "hardcore_negative_prompt"),
|
("hardcore_row", "hardcore_prompt", "hardcore_caption", "hardcore_negative_prompt"),
|
||||||
)
|
)
|
||||||
for row_key, prompt_key, caption_key, negative_key in mapping:
|
for row_key, prompt_key, caption_key, negative_key in mapping:
|
||||||
row = pair.get(row_key)
|
_sync_pair_root_row_field(pair, row_key, prompt_key, "prompt")
|
||||||
if not isinstance(row, dict):
|
_sync_pair_root_row_field(pair, row_key, caption_key, "caption")
|
||||||
continue
|
_sync_pair_root_row_field(pair, row_key, negative_key, "negative_prompt")
|
||||||
if prompt_key in pair:
|
|
||||||
row["prompt"] = pair.get(prompt_key, "")
|
|
||||||
if caption_key in pair:
|
|
||||||
row["caption"] = pair.get(caption_key, "")
|
|
||||||
if negative_key in pair:
|
|
||||||
row["negative_prompt"] = pair.get(negative_key, "")
|
|
||||||
return pair
|
return pair
|
||||||
|
|
||||||
|
|
||||||
@@ -133,12 +316,51 @@ def synchronize_pair_side_metadata(pair: dict[str, Any]) -> dict[str, Any]:
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
for row_key, keys in side_keys.items():
|
for row_key, keys in side_keys.items():
|
||||||
|
for key in keys:
|
||||||
|
_sync_pair_root_row_field(pair, row_key, key, key)
|
||||||
|
return pair
|
||||||
|
|
||||||
|
|
||||||
|
def synchronize_pair_camera_metadata(pair: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
mapping = {
|
||||||
|
"softcore_row": (
|
||||||
|
("softcore_camera_config", "camera_config"),
|
||||||
|
("softcore_camera_directive", "camera_directive"),
|
||||||
|
("softcore_camera_scene_directive", "camera_scene_directive"),
|
||||||
|
),
|
||||||
|
"hardcore_row": (
|
||||||
|
("hardcore_camera_config", "camera_config"),
|
||||||
|
("hardcore_camera_directive", "camera_directive"),
|
||||||
|
("hardcore_camera_scene_directive", "camera_scene_directive"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for row_key, keys in mapping.items():
|
||||||
|
for source_key, target_key in keys:
|
||||||
|
_sync_pair_root_row_field(pair, row_key, source_key, target_key)
|
||||||
|
return pair
|
||||||
|
|
||||||
|
|
||||||
|
def synchronize_pair_cast_metadata(pair: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
descriptors = pair.get("shared_cast_descriptors")
|
||||||
|
if isinstance(descriptors, list):
|
||||||
|
descriptor_list = [str(item).strip() for item in descriptors if str(item or "").strip()]
|
||||||
|
descriptor_text = "; ".join(descriptor_list)
|
||||||
|
else:
|
||||||
|
descriptor_text = str(descriptors or "").strip()
|
||||||
|
descriptor_list = [descriptor_text] if descriptor_text else []
|
||||||
|
if not descriptor_text:
|
||||||
|
return pair
|
||||||
|
|
||||||
|
options = pair.get("options") if isinstance(pair.get("options"), dict) else {}
|
||||||
|
row_keys = ["hardcore_row"]
|
||||||
|
if options.get("softcore_cast") == "same_as_hardcore":
|
||||||
|
row_keys.append("softcore_row")
|
||||||
|
for row_key in row_keys:
|
||||||
row = pair.get(row_key)
|
row = pair.get(row_key)
|
||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
continue
|
continue
|
||||||
for key in keys:
|
row["cast_descriptor_text"] = descriptor_text
|
||||||
if key in pair:
|
row["cast_descriptors"] = list(descriptor_list)
|
||||||
row[key] = pair.get(key)
|
|
||||||
return pair
|
return pair
|
||||||
|
|
||||||
|
|
||||||
@@ -147,6 +369,8 @@ def normalize_pair_metadata(pair: dict[str, Any], *, active_trigger: str = "") -
|
|||||||
triggers = _trigger_tuple(trigger)
|
triggers = _trigger_tuple(trigger)
|
||||||
synchronize_pair_row_outputs(pair)
|
synchronize_pair_row_outputs(pair)
|
||||||
synchronize_pair_side_metadata(pair)
|
synchronize_pair_side_metadata(pair)
|
||||||
|
synchronize_pair_camera_metadata(pair)
|
||||||
|
synchronize_pair_cast_metadata(pair)
|
||||||
for key in ("softcore_prompt", "hardcore_prompt"):
|
for key in ("softcore_prompt", "hardcore_prompt"):
|
||||||
if key in pair:
|
if key in pair:
|
||||||
pair[key] = sanitize_prompt_text(pair.get(key, ""), triggers=triggers)
|
pair[key] = sanitize_prompt_text(pair.get(key, ""), triggers=triggers)
|
||||||
|
|||||||
+40
-14
@@ -23,6 +23,7 @@ except ImportError: # Allows local smoke tests from the repository root.
|
|||||||
class PromptAxesRoute:
|
class PromptAxesRoute:
|
||||||
scene_slug: str
|
scene_slug: str
|
||||||
scene: str
|
scene: str
|
||||||
|
scene_entry: dict[str, Any]
|
||||||
pose: str
|
pose: str
|
||||||
expression: str
|
expression: str
|
||||||
shared_expression: str
|
shared_expression: str
|
||||||
@@ -30,11 +31,13 @@ class PromptAxesRoute:
|
|||||||
character_expression_text: str
|
character_expression_text: str
|
||||||
source_composition: str
|
source_composition: str
|
||||||
composition: str
|
composition: str
|
||||||
|
composition_entry: dict[str, Any]
|
||||||
|
|
||||||
def as_dict(self) -> dict[str, Any]:
|
def as_dict(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"scene_slug": self.scene_slug,
|
"scene_slug": self.scene_slug,
|
||||||
"scene": self.scene,
|
"scene": self.scene,
|
||||||
|
"scene_entry": dict(self.scene_entry),
|
||||||
"pose": self.pose,
|
"pose": self.pose,
|
||||||
"expression": self.expression,
|
"expression": self.expression,
|
||||||
"shared_expression": self.shared_expression,
|
"shared_expression": self.shared_expression,
|
||||||
@@ -42,9 +45,29 @@ class PromptAxesRoute:
|
|||||||
"character_expression_text": self.character_expression_text,
|
"character_expression_text": self.character_expression_text,
|
||||||
"source_composition": self.source_composition,
|
"source_composition": self.source_composition,
|
||||||
"composition": self.composition,
|
"composition": self.composition,
|
||||||
|
"composition_entry": dict(self.composition_entry),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_entry(value: Any, *, slug: str = "", text: str = "") -> dict[str, Any]:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
entry = dict(value)
|
||||||
|
elif isinstance(value, (list, tuple)) and len(value) == 2:
|
||||||
|
entry = {"slug": str(value[0]), "prompt": str(value[1])}
|
||||||
|
else:
|
||||||
|
entry = {"prompt": str(value or "")}
|
||||||
|
if slug:
|
||||||
|
entry["slug"] = slug
|
||||||
|
if text:
|
||||||
|
if "prompt" in entry:
|
||||||
|
entry["prompt"] = text
|
||||||
|
elif "text" in entry:
|
||||||
|
entry["text"] = text
|
||||||
|
else:
|
||||||
|
entry["prompt"] = text
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
def resolve_prompt_axes_result(
|
def resolve_prompt_axes_result(
|
||||||
*,
|
*,
|
||||||
category: dict[str, Any],
|
category: dict[str, Any],
|
||||||
@@ -75,14 +98,14 @@ def resolve_prompt_axes_result(
|
|||||||
character_slot_map = character_slot_map or {}
|
character_slot_map = character_slot_map or {}
|
||||||
pov_character_labels = pov_character_labels or []
|
pov_character_labels = pov_character_labels or []
|
||||||
|
|
||||||
scene_slug, scene = row_item_policy.choose_pair(
|
scene_entries = category_policy.compatible_entries(
|
||||||
scene_rng,
|
row_pool_policy.scene_pool(category, subcategory, item, subject_type, location_config),
|
||||||
category_policy.compatible_entries(
|
women_count,
|
||||||
row_pool_policy.scene_pool(category, subcategory, item, subject_type, location_config),
|
men_count,
|
||||||
women_count,
|
|
||||||
men_count,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
scene_choice = row_item_policy.weighted_choice(scene_rng, scene_entries)
|
||||||
|
scene_slug, scene = row_item_policy.pair_from(scene_choice)
|
||||||
|
scene_entry = _metadata_entry(scene_choice, slug=scene_slug, text=scene)
|
||||||
pose = str(
|
pose = str(
|
||||||
category_policy.merged_field(category, subcategory, item, "pose", "")
|
category_policy.merged_field(category, subcategory, item, "pose", "")
|
||||||
or context.get("fallback_pose")
|
or context.get("fallback_pose")
|
||||||
@@ -137,21 +160,23 @@ def resolve_prompt_axes_result(
|
|||||||
if character_expression_text:
|
if character_expression_text:
|
||||||
expression = character_expression_text
|
expression = character_expression_text
|
||||||
|
|
||||||
source_composition = row_item_policy.choose_text(
|
composition_entries = category_policy.compatible_entries(
|
||||||
composition_rng,
|
row_pool_policy.composition_pool(category, subcategory, item, subject_type, composition_config),
|
||||||
category_policy.compatible_entries(
|
women_count,
|
||||||
row_pool_policy.composition_pool(category, subcategory, item, subject_type, composition_config),
|
men_count,
|
||||||
women_count,
|
|
||||||
men_count,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
composition_choice = row_item_policy.weighted_choice(composition_rng, composition_entries)
|
||||||
|
source_composition = row_item_policy.item_text(composition_choice)
|
||||||
|
composition_entry = _metadata_entry(composition_choice, text=source_composition)
|
||||||
if is_pose_category:
|
if is_pose_category:
|
||||||
source_composition = sanitize_hardcore_environment_anchors(source_composition)
|
source_composition = sanitize_hardcore_environment_anchors(source_composition)
|
||||||
|
composition_entry["prompt"] = source_composition
|
||||||
composition = pov_policy.pov_composition_prompt(source_composition, pov_character_labels)
|
composition = pov_policy.pov_composition_prompt(source_composition, pov_character_labels)
|
||||||
|
|
||||||
return PromptAxesRoute(
|
return PromptAxesRoute(
|
||||||
scene_slug=scene_slug,
|
scene_slug=scene_slug,
|
||||||
scene=scene,
|
scene=scene,
|
||||||
|
scene_entry=scene_entry,
|
||||||
pose=pose,
|
pose=pose,
|
||||||
expression=expression,
|
expression=expression,
|
||||||
shared_expression=shared_expression,
|
shared_expression=shared_expression,
|
||||||
@@ -159,6 +184,7 @@ def resolve_prompt_axes_result(
|
|||||||
character_expression_text=character_expression_text,
|
character_expression_text=character_expression_text,
|
||||||
source_composition=source_composition,
|
source_composition=source_composition,
|
||||||
composition=composition,
|
composition=composition,
|
||||||
|
composition_entry=composition_entry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+24
-2
@@ -50,6 +50,27 @@ def empty_action_position_route() -> dict[str, Any]:
|
|||||||
return empty_action_position_route_result().as_dict()
|
return empty_action_position_route_result().as_dict()
|
||||||
|
|
||||||
|
|
||||||
|
def _primary_position_key(
|
||||||
|
position_keys: list[str],
|
||||||
|
metadata: dict[str, Any],
|
||||||
|
hardcore_position_config: dict[str, Any] | None,
|
||||||
|
) -> str:
|
||||||
|
if not position_keys:
|
||||||
|
return ""
|
||||||
|
configured = []
|
||||||
|
if isinstance(hardcore_position_config, dict):
|
||||||
|
configured = hardcore_position_policy.normalize_hardcore_position_values(
|
||||||
|
hardcore_position_config.get("positions")
|
||||||
|
)
|
||||||
|
for key in configured:
|
||||||
|
if key in position_keys:
|
||||||
|
return key
|
||||||
|
for key in template_policy.template_position_keys(metadata):
|
||||||
|
if key in position_keys:
|
||||||
|
return key
|
||||||
|
return position_keys[0]
|
||||||
|
|
||||||
|
|
||||||
def resolve_action_position_route_result(
|
def resolve_action_position_route_result(
|
||||||
*,
|
*,
|
||||||
is_pose_category: bool,
|
is_pose_category: bool,
|
||||||
@@ -83,7 +104,8 @@ def resolve_action_position_route_result(
|
|||||||
template_policy.template_position_keys(metadata),
|
template_policy.template_position_keys(metadata),
|
||||||
inferred_position_keys,
|
inferred_position_keys,
|
||||||
)
|
)
|
||||||
action_family = template_policy.template_action_family(metadata)
|
explicit_action_family = template_policy.template_action_family(metadata)
|
||||||
|
action_family = "" if explicit_action_family == "default" else explicit_action_family
|
||||||
if not action_family:
|
if not action_family:
|
||||||
action_family = source_hardcore_action_family(
|
action_family = source_hardcore_action_family(
|
||||||
position_family,
|
position_family,
|
||||||
@@ -96,7 +118,7 @@ def resolve_action_position_route_result(
|
|||||||
return ActionPositionRoute(
|
return ActionPositionRoute(
|
||||||
position_family=position_family,
|
position_family=position_family,
|
||||||
position_keys=position_keys,
|
position_keys=position_keys,
|
||||||
position_key=position_keys[0] if position_keys else "",
|
position_key=_primary_position_key(position_keys, metadata, hardcore_position_config),
|
||||||
action_family=action_family,
|
action_family=action_family,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+934
-101
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,203 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import formatter_input as input_policy
|
||||||
|
from . import formatter_route_trace as trace_policy
|
||||||
|
from . import formatter_target as target_policy
|
||||||
|
except ImportError: # pragma: no cover - plain-script smoke tests
|
||||||
|
import formatter_input as input_policy
|
||||||
|
import formatter_route_trace as trace_policy
|
||||||
|
import formatter_target as target_policy
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SDXLFormatRequest:
|
||||||
|
source_text: str
|
||||||
|
metadata_json: str = ""
|
||||||
|
negative_prompt: str = ""
|
||||||
|
input_hint: str = "auto"
|
||||||
|
target: str = "auto"
|
||||||
|
style_preset: str = "flat_vector_pony"
|
||||||
|
quality_preset: str = "pony_high"
|
||||||
|
trigger: str = "mythp0rt"
|
||||||
|
prepend_trigger: bool = True
|
||||||
|
preserve_trigger: bool = False
|
||||||
|
nude_weight: float = 1.29
|
||||||
|
custom_style: str = ""
|
||||||
|
custom_quality: str = ""
|
||||||
|
extra_positive: str = ""
|
||||||
|
extra_negative: str = ""
|
||||||
|
formatter_profile: str = "manual_controls"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SDXLFormatRoute:
|
||||||
|
output: dict[str, str]
|
||||||
|
branch: str
|
||||||
|
method: str
|
||||||
|
target: str
|
||||||
|
style_preset: str
|
||||||
|
quality_preset: str
|
||||||
|
nude_weight: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SDXLFormatDependencies:
|
||||||
|
default_negative: str
|
||||||
|
apply_formatter_profile: Callable[[str, str, str], tuple[str, str]]
|
||||||
|
clean: Callable[[Any], str]
|
||||||
|
row_from_inputs: Callable[[str, str, str], tuple[dict[str, Any] | None, str]]
|
||||||
|
row_core_tags: Callable[[dict[str, Any], float], list[str]]
|
||||||
|
soft_tags: Callable[[dict[str, Any], dict[str, Any], float], str]
|
||||||
|
hard_tags: Callable[[dict[str, Any], dict[str, Any], float], str]
|
||||||
|
fallback_text_to_sdxl: Callable[[str, bool, float], tuple[str, str, str]]
|
||||||
|
assemble_prompt: Callable[[str, str, str, str, bool, str, str, str], str]
|
||||||
|
combine_negative: Callable[..., str]
|
||||||
|
sanitize_negative_text: Callable[[str], str]
|
||||||
|
|
||||||
|
|
||||||
|
def format_sdxl_prompt_result(request: SDXLFormatRequest, deps: SDXLFormatDependencies) -> SDXLFormatRoute:
|
||||||
|
style_preset, quality_preset = deps.apply_formatter_profile(
|
||||||
|
request.formatter_profile,
|
||||||
|
request.style_preset,
|
||||||
|
request.quality_preset,
|
||||||
|
)
|
||||||
|
target = target_policy.normalize_target(request.target)
|
||||||
|
input_hint = input_policy.normalize_input_hint(request.input_hint, text_hint=input_policy.INPUT_HINT_PROMPT)
|
||||||
|
nude_weight = max(0.1, min(3.0, float(request.nude_weight)))
|
||||||
|
row, method = deps.row_from_inputs(request.source_text, request.metadata_json, request.input_hint)
|
||||||
|
|
||||||
|
if row and input_policy.is_pair_metadata(row):
|
||||||
|
pair_target = target_policy.pair_policy(target)
|
||||||
|
soft_row = row.get("softcore_row") if isinstance(row.get("softcore_row"), dict) else {}
|
||||||
|
hard_row = row.get("hardcore_row") if isinstance(row.get("hardcore_row"), dict) else {}
|
||||||
|
soft_body = deps.soft_tags(soft_row, row, nude_weight)
|
||||||
|
hard_body = deps.hard_tags(hard_row, row, nude_weight)
|
||||||
|
soft_prompt = deps.assemble_prompt(
|
||||||
|
soft_body,
|
||||||
|
style_preset,
|
||||||
|
quality_preset,
|
||||||
|
request.trigger,
|
||||||
|
request.prepend_trigger,
|
||||||
|
request.custom_style,
|
||||||
|
request.custom_quality,
|
||||||
|
request.extra_positive,
|
||||||
|
)
|
||||||
|
hard_prompt = deps.assemble_prompt(
|
||||||
|
hard_body,
|
||||||
|
style_preset,
|
||||||
|
quality_preset,
|
||||||
|
request.trigger,
|
||||||
|
request.prepend_trigger,
|
||||||
|
request.custom_style,
|
||||||
|
request.custom_quality,
|
||||||
|
request.extra_positive,
|
||||||
|
)
|
||||||
|
selected = hard_prompt if pair_target.selected_side == "hardcore" else soft_prompt
|
||||||
|
selected_negative = (
|
||||||
|
row.get("hardcore_negative_prompt")
|
||||||
|
if pair_target.selected_side == "hardcore"
|
||||||
|
else row.get("softcore_negative_prompt")
|
||||||
|
)
|
||||||
|
output = {
|
||||||
|
"sdxl_prompt": selected,
|
||||||
|
"negative_prompt": deps.sanitize_negative_text(
|
||||||
|
deps.combine_negative(
|
||||||
|
deps.default_negative,
|
||||||
|
selected_negative,
|
||||||
|
request.negative_prompt,
|
||||||
|
request.extra_negative,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"sdxl_softcore_prompt": soft_prompt,
|
||||||
|
"sdxl_hardcore_prompt": hard_prompt,
|
||||||
|
"softcore_negative_prompt": deps.sanitize_negative_text(
|
||||||
|
deps.combine_negative(deps.default_negative, row.get("softcore_negative_prompt"), request.extra_negative)
|
||||||
|
),
|
||||||
|
"hardcore_negative_prompt": deps.sanitize_negative_text(
|
||||||
|
deps.combine_negative(deps.default_negative, row.get("hardcore_negative_prompt"), request.extra_negative)
|
||||||
|
),
|
||||||
|
"method": f"{method}:sdxl(insta_of_pair)",
|
||||||
|
}
|
||||||
|
output["route_trace_json"] = trace_policy.route_trace_json(
|
||||||
|
formatter="sdxl",
|
||||||
|
branch="insta_of_pair",
|
||||||
|
method=output["method"],
|
||||||
|
input_hint=input_hint,
|
||||||
|
target=target,
|
||||||
|
style_preset=style_preset,
|
||||||
|
quality_preset=quality_preset,
|
||||||
|
nude_weight=nude_weight,
|
||||||
|
**trace_policy.metadata_trace_fields(row, target=target, selected_side=pair_target.selected_side),
|
||||||
|
)
|
||||||
|
return SDXLFormatRoute(
|
||||||
|
output=output,
|
||||||
|
branch="insta_of_pair",
|
||||||
|
method=output["method"],
|
||||||
|
target=target,
|
||||||
|
style_preset=style_preset,
|
||||||
|
quality_preset=quality_preset,
|
||||||
|
nude_weight=nude_weight,
|
||||||
|
)
|
||||||
|
|
||||||
|
if row:
|
||||||
|
body = ", ".join(deps.row_core_tags(row, nude_weight))
|
||||||
|
extracted_negative = deps.clean(row.get("negative_prompt"))
|
||||||
|
method = f"{method}:sdxl(metadata)"
|
||||||
|
branch = "metadata"
|
||||||
|
else:
|
||||||
|
body, extracted_negative, method = deps.fallback_text_to_sdxl(
|
||||||
|
request.source_text,
|
||||||
|
request.preserve_trigger,
|
||||||
|
nude_weight,
|
||||||
|
)
|
||||||
|
branch = "fallback"
|
||||||
|
|
||||||
|
prompt = deps.assemble_prompt(
|
||||||
|
body,
|
||||||
|
style_preset,
|
||||||
|
quality_preset,
|
||||||
|
request.trigger,
|
||||||
|
request.prepend_trigger,
|
||||||
|
request.custom_style,
|
||||||
|
request.custom_quality,
|
||||||
|
request.extra_positive,
|
||||||
|
)
|
||||||
|
output = {
|
||||||
|
"sdxl_prompt": prompt,
|
||||||
|
"negative_prompt": deps.sanitize_negative_text(
|
||||||
|
deps.combine_negative(deps.default_negative, extracted_negative, request.negative_prompt, request.extra_negative)
|
||||||
|
),
|
||||||
|
"sdxl_softcore_prompt": "",
|
||||||
|
"sdxl_hardcore_prompt": "",
|
||||||
|
"softcore_negative_prompt": "",
|
||||||
|
"hardcore_negative_prompt": "",
|
||||||
|
"method": method,
|
||||||
|
}
|
||||||
|
output["route_trace_json"] = trace_policy.route_trace_json(
|
||||||
|
formatter="sdxl",
|
||||||
|
branch=branch,
|
||||||
|
method=method,
|
||||||
|
input_hint=input_hint,
|
||||||
|
target=target,
|
||||||
|
style_preset=style_preset,
|
||||||
|
quality_preset=quality_preset,
|
||||||
|
nude_weight=nude_weight,
|
||||||
|
**trace_policy.metadata_trace_fields(row, target=target),
|
||||||
|
)
|
||||||
|
return SDXLFormatRoute(
|
||||||
|
output=output,
|
||||||
|
branch=branch,
|
||||||
|
method=method,
|
||||||
|
target=target,
|
||||||
|
style_preset=style_preset,
|
||||||
|
quality_preset=quality_preset,
|
||||||
|
nude_weight=nude_weight,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_sdxl_prompt(request: SDXLFormatRequest, deps: SDXLFormatDependencies) -> dict[str, str]:
|
||||||
|
return format_sdxl_prompt_result(request, deps).output
|
||||||
+42
-81
@@ -4,12 +4,14 @@ from typing import Any
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from . import formatter_input as input_policy
|
from . import formatter_input as input_policy
|
||||||
|
from . import sdxl_format_route
|
||||||
from . import sdxl_tag_policy
|
from . import sdxl_tag_policy
|
||||||
from . import sdxl_tag_routes
|
from . import sdxl_tag_routes
|
||||||
from . import sdxl_presets as sdxl_policy
|
from . import sdxl_presets as sdxl_policy
|
||||||
from .prompt_hygiene import sanitize_negative_text, sanitize_tag_prompt
|
from .prompt_hygiene import sanitize_negative_text, sanitize_tag_prompt
|
||||||
except ImportError: # Allows local smoke tests with `python -c`.
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
import formatter_input as input_policy
|
import formatter_input as input_policy
|
||||||
|
import sdxl_format_route
|
||||||
import sdxl_tag_policy
|
import sdxl_tag_policy
|
||||||
import sdxl_tag_routes
|
import sdxl_tag_routes
|
||||||
import sdxl_presets as sdxl_policy
|
import sdxl_presets as sdxl_policy
|
||||||
@@ -203,6 +205,26 @@ def _fallback_text_to_sdxl(
|
|||||||
return tags, negative, "text(fallback)"
|
return tags, negative, "text(fallback)"
|
||||||
|
|
||||||
|
|
||||||
|
def _sdxl_format_dependencies() -> sdxl_format_route.SDXLFormatDependencies:
|
||||||
|
return sdxl_format_route.SDXLFormatDependencies(
|
||||||
|
default_negative=SDXL_DEFAULT_NEGATIVE,
|
||||||
|
apply_formatter_profile=lambda profile, style, quality: sdxl_policy.apply_formatter_profile(
|
||||||
|
profile,
|
||||||
|
style_preset=style,
|
||||||
|
quality_preset=quality,
|
||||||
|
),
|
||||||
|
clean=_clean,
|
||||||
|
row_from_inputs=_row_from_inputs,
|
||||||
|
row_core_tags=_row_core_tags,
|
||||||
|
soft_tags=_soft_tags,
|
||||||
|
hard_tags=_hard_tags,
|
||||||
|
fallback_text_to_sdxl=_fallback_text_to_sdxl,
|
||||||
|
assemble_prompt=_assemble_prompt,
|
||||||
|
combine_negative=_combine_negative,
|
||||||
|
sanitize_negative_text=sanitize_negative_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_sdxl_prompt(
|
def format_sdxl_prompt(
|
||||||
source_text: str,
|
source_text: str,
|
||||||
metadata_json: str = "",
|
metadata_json: str = "",
|
||||||
@@ -221,85 +243,24 @@ def format_sdxl_prompt(
|
|||||||
extra_negative: str = "",
|
extra_negative: str = "",
|
||||||
formatter_profile: str = "manual_controls",
|
formatter_profile: str = "manual_controls",
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
style_preset, quality_preset = sdxl_policy.apply_formatter_profile(
|
return sdxl_format_route.format_sdxl_prompt(
|
||||||
formatter_profile,
|
sdxl_format_route.SDXLFormatRequest(
|
||||||
style_preset=style_preset,
|
source_text=source_text,
|
||||||
quality_preset=quality_preset,
|
metadata_json=metadata_json,
|
||||||
)
|
negative_prompt=negative_prompt,
|
||||||
target = target if target in ("auto", "single", "softcore", "hardcore") else "auto"
|
input_hint=input_hint,
|
||||||
nude_weight = max(0.1, min(3.0, float(nude_weight)))
|
target=target,
|
||||||
row, method = _row_from_inputs(source_text, metadata_json, input_hint)
|
style_preset=style_preset,
|
||||||
|
quality_preset=quality_preset,
|
||||||
if row and row.get("mode") == "Insta/OF":
|
trigger=trigger,
|
||||||
soft_row = row.get("softcore_row") if isinstance(row.get("softcore_row"), dict) else {}
|
prepend_trigger=prepend_trigger,
|
||||||
hard_row = row.get("hardcore_row") if isinstance(row.get("hardcore_row"), dict) else {}
|
preserve_trigger=preserve_trigger,
|
||||||
soft_body = _soft_tags(soft_row, row, nude_weight)
|
nude_weight=nude_weight,
|
||||||
hard_body = _hard_tags(hard_row, row, nude_weight)
|
custom_style=custom_style,
|
||||||
soft_prompt = _assemble_prompt(
|
custom_quality=custom_quality,
|
||||||
soft_body,
|
extra_positive=extra_positive,
|
||||||
style_preset,
|
extra_negative=extra_negative,
|
||||||
quality_preset,
|
formatter_profile=formatter_profile,
|
||||||
trigger,
|
|
||||||
prepend_trigger,
|
|
||||||
custom_style,
|
|
||||||
custom_quality,
|
|
||||||
extra_positive,
|
|
||||||
)
|
|
||||||
hard_prompt = _assemble_prompt(
|
|
||||||
hard_body,
|
|
||||||
style_preset,
|
|
||||||
quality_preset,
|
|
||||||
trigger,
|
|
||||||
prepend_trigger,
|
|
||||||
custom_style,
|
|
||||||
custom_quality,
|
|
||||||
extra_positive,
|
|
||||||
)
|
|
||||||
selected = hard_prompt if target == "hardcore" else soft_prompt
|
|
||||||
selected_negative = (
|
|
||||||
row.get("hardcore_negative_prompt") if target == "hardcore" else row.get("softcore_negative_prompt")
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"sdxl_prompt": selected,
|
|
||||||
"negative_prompt": sanitize_negative_text(
|
|
||||||
_combine_negative(SDXL_DEFAULT_NEGATIVE, selected_negative, negative_prompt, extra_negative)
|
|
||||||
),
|
|
||||||
"sdxl_softcore_prompt": soft_prompt,
|
|
||||||
"sdxl_hardcore_prompt": hard_prompt,
|
|
||||||
"softcore_negative_prompt": sanitize_negative_text(
|
|
||||||
_combine_negative(SDXL_DEFAULT_NEGATIVE, row.get("softcore_negative_prompt"), extra_negative)
|
|
||||||
),
|
|
||||||
"hardcore_negative_prompt": sanitize_negative_text(
|
|
||||||
_combine_negative(SDXL_DEFAULT_NEGATIVE, row.get("hardcore_negative_prompt"), extra_negative)
|
|
||||||
),
|
|
||||||
"method": f"{method}:sdxl(insta_of_pair)",
|
|
||||||
}
|
|
||||||
|
|
||||||
if row:
|
|
||||||
body = ", ".join(_row_core_tags(row, nude_weight))
|
|
||||||
extracted_negative = _clean(row.get("negative_prompt"))
|
|
||||||
method = f"{method}:sdxl(metadata)"
|
|
||||||
else:
|
|
||||||
body, extracted_negative, method = _fallback_text_to_sdxl(source_text, preserve_trigger, nude_weight)
|
|
||||||
|
|
||||||
prompt = _assemble_prompt(
|
|
||||||
body,
|
|
||||||
style_preset,
|
|
||||||
quality_preset,
|
|
||||||
trigger,
|
|
||||||
prepend_trigger,
|
|
||||||
custom_style,
|
|
||||||
custom_quality,
|
|
||||||
extra_positive,
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"sdxl_prompt": prompt,
|
|
||||||
"negative_prompt": sanitize_negative_text(
|
|
||||||
_combine_negative(SDXL_DEFAULT_NEGATIVE, extracted_negative, negative_prompt, extra_negative)
|
|
||||||
),
|
),
|
||||||
"sdxl_softcore_prompt": "",
|
_sdxl_format_dependencies(),
|
||||||
"sdxl_hardcore_prompt": "",
|
)
|
||||||
"softcore_negative_prompt": "",
|
|
||||||
"hardcore_negative_prompt": "",
|
|
||||||
"method": method,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_STYLE_PRESET = "flat_vector_pony"
|
DEFAULT_STYLE_PRESET = "flat_vector_pony"
|
||||||
DEFAULT_QUALITY_PRESET = "pony_high"
|
DEFAULT_QUALITY_PRESET = "pony_high"
|
||||||
@@ -45,10 +47,14 @@ SDXL_DEFAULT_NEGATIVE = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
SDXL_ACTION_FAMILY_TAGS = {
|
SDXL_ACTION_FAMILY_TAGS = {
|
||||||
|
"anal": ("anal sex",),
|
||||||
"foreplay": ("foreplay", "body contact"),
|
"foreplay": ("foreplay", "body contact"),
|
||||||
|
"manual": ("manual stimulation",),
|
||||||
"outercourse": ("outercourse", "non-penetrative sex"),
|
"outercourse": ("outercourse", "non-penetrative sex"),
|
||||||
"oral": ("oral sex",),
|
"oral": ("oral sex",),
|
||||||
"penetration": ("penetrative sex", "penetration"),
|
"penetration": ("penetrative sex", "penetration"),
|
||||||
|
"threesome": ("threesome",),
|
||||||
|
"group": ("group sex",),
|
||||||
"toy_double": ("double penetration", "toy-assisted sex"),
|
"toy_double": ("double penetration", "toy-assisted sex"),
|
||||||
"climax": ("climax", "semen"),
|
"climax": ("climax", "semen"),
|
||||||
}
|
}
|
||||||
@@ -79,15 +85,22 @@ def sdxl_formatter_profile_choices() -> list[str]:
|
|||||||
return list(SDXL_FORMATTER_PROFILES)
|
return list(SDXL_FORMATTER_PROFILES)
|
||||||
|
|
||||||
|
|
||||||
|
def _choice_key(value: Any) -> str:
|
||||||
|
return str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
||||||
|
|
||||||
|
|
||||||
def normalize_style_preset(value: str) -> str:
|
def normalize_style_preset(value: str) -> str:
|
||||||
|
value = _choice_key(value)
|
||||||
return value if value in SDXL_STYLE_PRESETS else DEFAULT_STYLE_PRESET
|
return value if value in SDXL_STYLE_PRESETS else DEFAULT_STYLE_PRESET
|
||||||
|
|
||||||
|
|
||||||
def normalize_quality_preset(value: str) -> str:
|
def normalize_quality_preset(value: str) -> str:
|
||||||
|
value = _choice_key(value)
|
||||||
return value if value in SDXL_QUALITY_PRESETS else DEFAULT_QUALITY_PRESET
|
return value if value in SDXL_QUALITY_PRESETS else DEFAULT_QUALITY_PRESET
|
||||||
|
|
||||||
|
|
||||||
def normalize_formatter_profile(value: str) -> str:
|
def normalize_formatter_profile(value: str) -> str:
|
||||||
|
value = _choice_key(value)
|
||||||
return value if value in SDXL_FORMATTER_PROFILES else DEFAULT_FORMATTER_PROFILE
|
return value if value in SDXL_FORMATTER_PROFILES else DEFAULT_FORMATTER_PROFILE
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+90
-8
@@ -5,18 +5,35 @@ from typing import Any
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from . import formatter_input as input_policy
|
from . import formatter_input as input_policy
|
||||||
|
from . import item_axis_policy
|
||||||
from . import route_metadata as route_metadata_policy
|
from . import route_metadata as route_metadata_policy
|
||||||
from . import sdxl_presets as sdxl_policy
|
from . import sdxl_presets as sdxl_policy
|
||||||
from . import sdxl_tag_routes
|
from . import sdxl_tag_routes
|
||||||
|
from . import softcore_text_policy
|
||||||
except ImportError: # Allows local smoke tests with `python -c`.
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
import formatter_input as input_policy
|
import formatter_input as input_policy
|
||||||
|
import item_axis_policy
|
||||||
import route_metadata as route_metadata_policy
|
import route_metadata as route_metadata_policy
|
||||||
import sdxl_presets as sdxl_policy
|
import sdxl_presets as sdxl_policy
|
||||||
import sdxl_tag_routes
|
import sdxl_tag_routes
|
||||||
|
import softcore_text_policy
|
||||||
|
|
||||||
|
|
||||||
PROMPT_FIELD_LABELS = input_policy.prompt_field_labels()
|
PROMPT_FIELD_LABELS = input_policy.prompt_field_labels()
|
||||||
|
|
||||||
|
INCOMPATIBLE_ROUTE_TAGS = {
|
||||||
|
"action:anal": ("oral sex", "outercourse", "manual stimulation"),
|
||||||
|
"action:penetration": ("oral sex", "outercourse", "anal sex", "manual stimulation"),
|
||||||
|
"action:oral": ("penetrative sex", "penetration", "anal sex", "outercourse"),
|
||||||
|
"action:outercourse": ("penetrative sex", "penetration", "oral sex", "anal sex", "manual stimulation"),
|
||||||
|
"action:manual": ("penetrative sex", "penetration", "oral sex", "anal sex", "outercourse"),
|
||||||
|
"position:penetrative": ("oral sex", "outercourse", "anal sex", "manual stimulation"),
|
||||||
|
"position:oral": ("penetrative sex", "penetration", "anal sex", "outercourse"),
|
||||||
|
"position:outercourse": ("penetrative sex", "penetration", "oral sex", "anal sex", "manual stimulation"),
|
||||||
|
"position:manual": ("penetrative sex", "penetration", "oral sex", "anal sex", "outercourse"),
|
||||||
|
"position:anal": ("oral sex", "outercourse", "manual stimulation"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def clean(value: Any) -> str:
|
def clean(value: Any) -> str:
|
||||||
return input_policy.clean_text(value)
|
return input_policy.clean_text(value)
|
||||||
@@ -34,19 +51,28 @@ def split_tag_text(text: Any) -> list[str]:
|
|||||||
text = clean(text)
|
text = clean(text)
|
||||||
if not text:
|
if not text:
|
||||||
return []
|
return []
|
||||||
|
text = input_policy.strip_prompt_field_labels(text, field_labels=PROMPT_FIELD_LABELS)
|
||||||
text = re.sub(r"\bWoman [A-Z]'s\b", "woman's", text)
|
text = re.sub(r"\bWoman [A-Z]'s\b", "woman's", text)
|
||||||
text = re.sub(r"\bMan [A-Z]'s\b", "man's", text)
|
text = re.sub(r"\bMan [A-Z]'s\b", "man's", text)
|
||||||
text = re.sub(r"\bWoman [A-Z]\b", "woman", text)
|
text = re.sub(r"\bWoman [A-Z]\b", "woman", text)
|
||||||
text = re.sub(r"\bMan [A-Z]\b", "man", text)
|
text = re.sub(r"\bMan [A-Z]\b", "man", text)
|
||||||
|
text = re.sub(r"\b(?:the\s+)?(?:woman|man)\s+has\s+", "", text, flags=re.IGNORECASE)
|
||||||
text = re.sub(
|
text = re.sub(
|
||||||
r"\b(?:Clothing state|Visual clothing state|visible remaining styling|teaser outfit detail|softcore visual reference|Sexual scene|Role graph):\s*",
|
r"\b(?:Clothing state|Visual clothing state|visible remaining styling|teaser outfit detail|softcore visual reference|Sexual scene|Role graph):\s*",
|
||||||
"",
|
"",
|
||||||
text,
|
text,
|
||||||
flags=re.IGNORECASE,
|
flags=re.IGNORECASE,
|
||||||
)
|
)
|
||||||
text = re.sub(r"\b(?:and|with)\b", ",", text, flags=re.IGNORECASE)
|
text = re.sub(r"(?<=[A-Za-z0-9)])\.\s+(?=[A-Za-z])", ", ", text)
|
||||||
|
text = re.sub(r"(?<!-)\b(?:and|with)\b(?!-)", ",", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r"\b(woman|man),\s+(woman|man)\s+are\b", r"\1 and \2 are", text, flags=re.IGNORECASE)
|
||||||
parts = re.split(r"\s*[,;]\s*", text)
|
parts = re.split(r"\s*[,;]\s*", text)
|
||||||
return [clean(part).strip(" .") for part in parts if clean(part).strip(" .")]
|
tags = []
|
||||||
|
for part in parts:
|
||||||
|
part = re.sub(r"^keep\s+", "", clean(part).strip(" ."), flags=re.IGNORECASE)
|
||||||
|
if part:
|
||||||
|
tags.append(part)
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
def tag_key(tag: str) -> str:
|
def tag_key(tag: str) -> str:
|
||||||
@@ -99,6 +125,14 @@ def formatter_hint_tags(*rows: dict[str, Any]) -> list[str]:
|
|||||||
return tags
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
def axis_value_tags(row: dict[str, Any]) -> list[str]:
|
||||||
|
tags: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for text in item_axis_policy.row_axis_value_texts(row):
|
||||||
|
add(tags, seen, text)
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
def combine_tags(*parts: Any) -> str:
|
def combine_tags(*parts: Any) -> str:
|
||||||
tags: list[str] = []
|
tags: list[str] = []
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
@@ -159,11 +193,7 @@ def character_tags_from_descriptor(descriptor: Any) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def normal_character_tags(row: dict[str, Any]) -> list[str]:
|
def normal_character_tags(row: dict[str, Any]) -> list[str]:
|
||||||
descriptor = (
|
descriptor = clean(row.get("cast_descriptor_text"))
|
||||||
clean(row.get("cast_descriptor_text"))
|
|
||||||
or prompt_field(row.get("prompt", ""), "Characters")
|
|
||||||
or prompt_field(row.get("prompt", ""), "Cast descriptors")
|
|
||||||
)
|
|
||||||
if descriptor:
|
if descriptor:
|
||||||
return character_tags_from_descriptor(descriptor)
|
return character_tags_from_descriptor(descriptor)
|
||||||
|
|
||||||
@@ -229,7 +259,33 @@ def explicit_tags(text: str, nude_weight: float) -> list[str]:
|
|||||||
tags.append("penetration")
|
tags.append("penetration")
|
||||||
if "vaginal" in lower:
|
if "vaginal" in lower:
|
||||||
tags.append("pussy")
|
tags.append("pussy")
|
||||||
if "oral" in lower or "mouth" in lower:
|
oral_terms = (
|
||||||
|
"oral sex",
|
||||||
|
"oral-sex",
|
||||||
|
"blowjob",
|
||||||
|
"deepthroat",
|
||||||
|
"fellatio",
|
||||||
|
"cunnilingus",
|
||||||
|
"pussy licking",
|
||||||
|
"mouth on",
|
||||||
|
"mouth pressed",
|
||||||
|
"mouth contact",
|
||||||
|
"mouth around",
|
||||||
|
"lips wrapped",
|
||||||
|
"takes the penis in her mouth",
|
||||||
|
"takes the man's penis",
|
||||||
|
"takes the viewer's penis",
|
||||||
|
"penis in her mouth",
|
||||||
|
"tongue on pussy",
|
||||||
|
"tongue along the penis",
|
||||||
|
"tongue along the penis shaft",
|
||||||
|
"tongue touches the underside",
|
||||||
|
"licking the penis",
|
||||||
|
"testicle sucking",
|
||||||
|
"balls licking",
|
||||||
|
"balls-licking",
|
||||||
|
)
|
||||||
|
if any(token in lower for token in oral_terms):
|
||||||
tags.append("oral sex")
|
tags.append("oral sex")
|
||||||
if "anal" in lower:
|
if "anal" in lower:
|
||||||
tags.append("anal sex")
|
tags.append("anal sex")
|
||||||
@@ -238,6 +294,29 @@ def explicit_tags(text: str, nude_weight: float) -> list[str]:
|
|||||||
return tags
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
def filter_incompatible_route_tags(tags: list[str], row: dict[str, Any]) -> list[str]:
|
||||||
|
action_family = route_metadata_policy.row_action_family(row)
|
||||||
|
position_family = route_metadata_policy.row_position_family(row)
|
||||||
|
blocked: set[str] = set()
|
||||||
|
for scope, family in (("action", action_family), ("position", position_family)):
|
||||||
|
for tag in INCOMPATIBLE_ROUTE_TAGS.get(f"{scope}:{family}", ()):
|
||||||
|
blocked.add(tag_key(tag))
|
||||||
|
if not blocked:
|
||||||
|
return tags
|
||||||
|
return [tag for tag in tags if tag_key(tag) not in blocked]
|
||||||
|
|
||||||
|
|
||||||
|
def softcore_pair_tags(row: dict[str, Any], root: dict[str, Any]) -> list[str]:
|
||||||
|
tags = ["softcore teaser", softcore_text_policy.softcore_style_tag()]
|
||||||
|
options = root.get("options") if isinstance(root.get("options"), dict) else {}
|
||||||
|
cast_mode = clean(options.get("softcore_cast")).lower()
|
||||||
|
if cast_mode == "same_as_hardcore" or root.get("shared_cast_descriptors"):
|
||||||
|
tags.append("same-cast creator frame")
|
||||||
|
elif "solo" in clean(row.get("subject_type") or row.get("primary_subject")).lower():
|
||||||
|
tags.append("solo creator frame")
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
def tag_route_dependencies() -> sdxl_tag_routes.SDXLTagRouteDependencies:
|
def tag_route_dependencies() -> sdxl_tag_routes.SDXLTagRouteDependencies:
|
||||||
return sdxl_tag_routes.SDXLTagRouteDependencies(
|
return sdxl_tag_routes.SDXLTagRouteDependencies(
|
||||||
clean=clean,
|
clean=clean,
|
||||||
@@ -251,6 +330,9 @@ def tag_route_dependencies() -> sdxl_tag_routes.SDXLTagRouteDependencies:
|
|||||||
character_tags_from_descriptor=character_tags_from_descriptor,
|
character_tags_from_descriptor=character_tags_from_descriptor,
|
||||||
metadata_family_tags=metadata_family_tags,
|
metadata_family_tags=metadata_family_tags,
|
||||||
formatter_hint_tags=formatter_hint_tags,
|
formatter_hint_tags=formatter_hint_tags,
|
||||||
|
axis_value_tags=axis_value_tags,
|
||||||
camera_tags=camera_tags,
|
camera_tags=camera_tags,
|
||||||
explicit_tags=explicit_tags,
|
explicit_tags=explicit_tags,
|
||||||
|
filter_incompatible_route_tags=filter_incompatible_route_tags,
|
||||||
|
softcore_pair_tags=softcore_pair_tags,
|
||||||
)
|
)
|
||||||
|
|||||||
+105
-11
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
@@ -38,8 +39,76 @@ class SDXLTagRouteDependencies:
|
|||||||
character_tags_from_descriptor: Callable[[Any], list[str]]
|
character_tags_from_descriptor: Callable[[Any], list[str]]
|
||||||
metadata_family_tags: Callable[[dict[str, Any]], list[str]]
|
metadata_family_tags: Callable[[dict[str, Any]], list[str]]
|
||||||
formatter_hint_tags: Callable[..., list[str]]
|
formatter_hint_tags: Callable[..., list[str]]
|
||||||
|
axis_value_tags: Callable[[dict[str, Any]], list[str]]
|
||||||
camera_tags: Callable[..., list[str]]
|
camera_tags: Callable[..., list[str]]
|
||||||
explicit_tags: Callable[[str, float], list[str]]
|
explicit_tags: Callable[[str, float], list[str]]
|
||||||
|
filter_incompatible_route_tags: Callable[[list[str], dict[str, Any]], list[str]]
|
||||||
|
softcore_pair_tags: Callable[[dict[str, Any], dict[str, Any]], list[str]]
|
||||||
|
|
||||||
|
|
||||||
|
def _descriptor_counts(root: dict[str, Any]) -> tuple[int, int]:
|
||||||
|
descriptors = root.get("shared_cast_descriptors")
|
||||||
|
if not isinstance(descriptors, list):
|
||||||
|
return 0, 0
|
||||||
|
women = 0
|
||||||
|
men = 0
|
||||||
|
for descriptor in descriptors:
|
||||||
|
text = str(descriptor).lower()
|
||||||
|
if re.search(r"\bwoman\b", text):
|
||||||
|
women += 1
|
||||||
|
elif re.search(r"\bman\b", text):
|
||||||
|
men += 1
|
||||||
|
return women, men
|
||||||
|
|
||||||
|
|
||||||
|
def _pair_counts(row: dict[str, Any], root: dict[str, Any]) -> tuple[int, int]:
|
||||||
|
try:
|
||||||
|
women = int(root.get("hardcore_women_count") or row.get("women_count") or 0)
|
||||||
|
men = int(root.get("hardcore_men_count") or row.get("men_count") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
women, men = 0, 0
|
||||||
|
if women or men:
|
||||||
|
return women, men
|
||||||
|
return _descriptor_counts(root)
|
||||||
|
|
||||||
|
|
||||||
|
def _composition_tags_text(text: str) -> str:
|
||||||
|
text = re.sub(r"^vertical\s+", "", str(text or "").strip(), flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r"\s+composition$", "", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r"\bcomposition\b", "frame", text, flags=re.IGNORECASE)
|
||||||
|
return text.strip(" ,")
|
||||||
|
|
||||||
|
|
||||||
|
def _row_explicit_signal_text(
|
||||||
|
row: dict[str, Any],
|
||||||
|
*,
|
||||||
|
item: str,
|
||||||
|
pose: str,
|
||||||
|
role_graph: str,
|
||||||
|
expression: str,
|
||||||
|
composition: str,
|
||||||
|
deps: SDXLTagRouteDependencies,
|
||||||
|
) -> str:
|
||||||
|
values = (
|
||||||
|
item,
|
||||||
|
pose,
|
||||||
|
role_graph,
|
||||||
|
deps.clean(row.get("hardcore_clothing_state")),
|
||||||
|
deps.clean(row.get("clothing_state")),
|
||||||
|
deps.clean(row.get("clothing")),
|
||||||
|
deps.clean(row.get("scene_kind")),
|
||||||
|
expression,
|
||||||
|
composition,
|
||||||
|
)
|
||||||
|
return " ".join(deps.clean(value) for value in values if deps.clean(value))
|
||||||
|
|
||||||
|
|
||||||
|
def _uses_hardcore_action_route(row: dict[str, Any]) -> bool:
|
||||||
|
return (
|
||||||
|
str(row.get("category_slug") or "").strip() == "hardcore_sexual_poses"
|
||||||
|
or bool(str(row.get("action_family") or "").strip())
|
||||||
|
or bool(str(row.get("position_family") or "").strip())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def row_core_tags_result(request: SDXLRowTagRequest, deps: SDXLTagRouteDependencies) -> SDXLTagRoute:
|
def row_core_tags_result(request: SDXLRowTagRequest, deps: SDXLTagRouteDependencies) -> SDXLTagRoute:
|
||||||
@@ -57,11 +126,13 @@ def row_core_tags_result(request: SDXLRowTagRequest, deps: SDXLTagRouteDependenc
|
|||||||
deps.add_one(tags, seen, tag)
|
deps.add_one(tags, seen, tag)
|
||||||
for tag in deps.formatter_hint_tags(row):
|
for tag in deps.formatter_hint_tags(row):
|
||||||
deps.add(tags, seen, tag)
|
deps.add(tags, seen, tag)
|
||||||
|
for tag in deps.axis_value_tags(row):
|
||||||
|
deps.add(tags, seen, tag)
|
||||||
|
|
||||||
item = deps.row_value(row, "item", ("Sexual scene", "Sexual pose", "Erotic outfit", "Clothing")) or deps.clean(
|
item = deps.row_value(row, "item", ("Sexual scene", "Sexual pose", "Erotic outfit", "Clothing")) or deps.clean(
|
||||||
row.get("custom_item")
|
row.get("custom_item")
|
||||||
)
|
)
|
||||||
pose = deps.row_value(row, "pose", ("Sexual pose", "Pose"))
|
pose = "" if _uses_hardcore_action_route(row) else deps.row_value(row, "pose", ("Sexual pose", "Pose"))
|
||||||
role_graph = deps.clean(row.get("source_role_graph") or row.get("role_graph"))
|
role_graph = deps.clean(row.get("source_role_graph") or row.get("role_graph"))
|
||||||
scene = deps.row_value(row, "scene_text", ("Setting", "Scene")) or deps.clean(row.get("scene"))
|
scene = deps.row_value(row, "scene_text", ("Setting", "Scene")) or deps.clean(row.get("scene"))
|
||||||
expression = deps.row_value(row, "character_expression_text") or deps.row_value(
|
expression = deps.row_value(row, "character_expression_text") or deps.row_value(
|
||||||
@@ -69,7 +140,7 @@ def row_core_tags_result(request: SDXLRowTagRequest, deps: SDXLTagRouteDependenc
|
|||||||
"expression",
|
"expression",
|
||||||
("Facial expressions", "Facial expression"),
|
("Facial expressions", "Facial expression"),
|
||||||
)
|
)
|
||||||
composition = deps.row_value(row, "composition", ("Composition",))
|
composition = _composition_tags_text(deps.row_value(row, "composition", ("Composition",)))
|
||||||
for value in (
|
for value in (
|
||||||
item,
|
item,
|
||||||
pose,
|
pose,
|
||||||
@@ -82,9 +153,18 @@ def row_core_tags_result(request: SDXLRowTagRequest, deps: SDXLTagRouteDependenc
|
|||||||
for tag in deps.camera_tags(row):
|
for tag in deps.camera_tags(row):
|
||||||
deps.add_one(tags, seen, tag)
|
deps.add_one(tags, seen, tag)
|
||||||
|
|
||||||
combined = " ".join(deps.clean(value) for value in (item, pose, role_graph, row.get("prompt", "")))
|
combined = _row_explicit_signal_text(
|
||||||
|
row,
|
||||||
|
item=item,
|
||||||
|
pose=pose,
|
||||||
|
role_graph=role_graph,
|
||||||
|
expression=expression,
|
||||||
|
composition=composition,
|
||||||
|
deps=deps,
|
||||||
|
)
|
||||||
for tag in deps.explicit_tags(combined, request.nude_weight):
|
for tag in deps.explicit_tags(combined, request.nude_weight):
|
||||||
deps.add_one(tags, seen, tag)
|
deps.add_one(tags, seen, tag)
|
||||||
|
tags = deps.filter_incompatible_route_tags(tags, row)
|
||||||
return SDXLTagRoute(tags)
|
return SDXLTagRoute(tags)
|
||||||
|
|
||||||
|
|
||||||
@@ -93,16 +173,29 @@ def soft_tags_result(request: SDXLPairTagRequest, deps: SDXLTagRouteDependencies
|
|||||||
root = request.root
|
root = request.root
|
||||||
tags = row_core_tags_result(SDXLRowTagRequest(row, request.nude_weight), deps).tags
|
tags = row_core_tags_result(SDXLRowTagRequest(row, request.nude_weight), deps).tags
|
||||||
seen = {deps.tag_key(tag) for tag in tags}
|
seen = {deps.tag_key(tag) for tag in tags}
|
||||||
|
women, men = _pair_counts(row, root)
|
||||||
|
for tag in deps.count_tag(women, men):
|
||||||
|
deps.add_one(tags, seen, tag)
|
||||||
|
|
||||||
for tag in deps.formatter_hint_tags(root):
|
for tag in deps.formatter_hint_tags(root):
|
||||||
deps.add(tags, seen, tag)
|
deps.add(tags, seen, tag)
|
||||||
descriptor = deps.clean(root.get("shared_descriptor"))
|
|
||||||
if descriptor and not any("woman" in deps.tag_key(tag) for tag in tags):
|
descriptors = root.get("shared_cast_descriptors")
|
||||||
|
if isinstance(descriptors, list) and descriptors:
|
||||||
|
for descriptor in descriptors:
|
||||||
|
for tag in deps.character_tags_from_descriptor(descriptor):
|
||||||
|
deps.add_one(tags, seen, tag)
|
||||||
|
else:
|
||||||
|
descriptor = deps.clean(root.get("shared_descriptor"))
|
||||||
for tag in deps.character_tags_from_descriptor(descriptor):
|
for tag in deps.character_tags_from_descriptor(descriptor):
|
||||||
deps.add_one(tags, seen, tag)
|
deps.add_one(tags, seen, tag)
|
||||||
|
|
||||||
partner = root.get("softcore_partner_styling")
|
partner = root.get("softcore_partner_styling")
|
||||||
if isinstance(partner, dict):
|
if isinstance(partner, dict):
|
||||||
deps.add(tags, seen, "; ".join(deps.clean(item) for item in partner.get("outfits", []) if deps.clean(item)))
|
deps.add(tags, seen, "; ".join(deps.clean(item) for item in partner.get("outfits", []) if deps.clean(item)))
|
||||||
deps.add(tags, seen, partner.get("pose"))
|
deps.add(tags, seen, partner.get("pose"))
|
||||||
|
for tag in deps.softcore_pair_tags(row, root):
|
||||||
|
deps.add_one(tags, seen, tag)
|
||||||
deps.add_one(tags, seen, "sexy")
|
deps.add_one(tags, seen, "sexy")
|
||||||
deps.add_one(tags, seen, "looking at viewer")
|
deps.add_one(tags, seen, "looking at viewer")
|
||||||
return SDXLTagRoute(tags)
|
return SDXLTagRoute(tags)
|
||||||
@@ -113,10 +206,8 @@ def hard_tags_result(request: SDXLPairTagRequest, deps: SDXLTagRouteDependencies
|
|||||||
root = request.root
|
root = request.root
|
||||||
tags: list[str] = []
|
tags: list[str] = []
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
try:
|
women, men = _pair_counts(row, root)
|
||||||
women = int(root.get("hardcore_women_count") or row.get("women_count") or 1)
|
if not women and not men:
|
||||||
men = int(root.get("hardcore_men_count") or row.get("men_count") or 1)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
women, men = 1, 1
|
women, men = 1, 1
|
||||||
for tag in deps.count_tag(women, men):
|
for tag in deps.count_tag(women, men):
|
||||||
deps.add_one(tags, seen, tag)
|
deps.add_one(tags, seen, tag)
|
||||||
@@ -134,13 +225,15 @@ def hard_tags_result(request: SDXLPairTagRequest, deps: SDXLTagRouteDependencies
|
|||||||
deps.add_one(tags, seen, tag)
|
deps.add_one(tags, seen, tag)
|
||||||
for tag in deps.formatter_hint_tags(row, root):
|
for tag in deps.formatter_hint_tags(row, root):
|
||||||
deps.add(tags, seen, tag)
|
deps.add(tags, seen, tag)
|
||||||
|
for tag in deps.axis_value_tags(row):
|
||||||
|
deps.add(tags, seen, tag)
|
||||||
|
|
||||||
hard_scene = deps.clean(row.get("scene_text"))
|
hard_scene = deps.clean(row.get("scene_text"))
|
||||||
hard_item = deps.clean(row.get("item"))
|
hard_item = deps.clean(row.get("item"))
|
||||||
hard_role = deps.clean(row.get("source_role_graph") or row.get("role_graph"))
|
hard_role = deps.clean(row.get("source_role_graph") or row.get("role_graph"))
|
||||||
hard_clothing = deps.clean(root.get("hardcore_clothing_state"))
|
hard_clothing = deps.clean(root.get("hardcore_clothing_state"))
|
||||||
expression = deps.clean(row.get("character_expression_text") or row.get("expression"))
|
expression = deps.clean(row.get("character_expression_text") or row.get("expression"))
|
||||||
composition = deps.clean(row.get("composition"))
|
composition = _composition_tags_text(deps.clean(row.get("composition")))
|
||||||
for value in (
|
for value in (
|
||||||
hard_role,
|
hard_role,
|
||||||
hard_item,
|
hard_item,
|
||||||
@@ -152,9 +245,10 @@ def hard_tags_result(request: SDXLPairTagRequest, deps: SDXLTagRouteDependencies
|
|||||||
deps.add(tags, seen, value)
|
deps.add(tags, seen, value)
|
||||||
for tag in deps.camera_tags(row, root.get("hardcore_camera_directive"), root.get("hardcore_camera_config")):
|
for tag in deps.camera_tags(row, root.get("hardcore_camera_directive"), root.get("hardcore_camera_config")):
|
||||||
deps.add_one(tags, seen, tag)
|
deps.add_one(tags, seen, tag)
|
||||||
combined = " ".join([hard_role, hard_item, hard_clothing, expression, composition, root.get("hardcore_prompt", "") or ""])
|
combined = " ".join([hard_role, hard_item, hard_clothing, expression, composition])
|
||||||
for tag in deps.explicit_tags(combined, request.nude_weight):
|
for tag in deps.explicit_tags(combined, request.nude_weight):
|
||||||
deps.add_one(tags, seen, tag)
|
deps.add_one(tags, seen, tag)
|
||||||
|
tags = deps.filter_incompatible_route_tags(tags, row)
|
||||||
return SDXLTagRoute(tags)
|
return SDXLTagRoute(tags)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+91
-17
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
from typing import Any
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
|
||||||
SEED_AXIS_SALTS = {
|
SEED_AXIS_SALTS = {
|
||||||
@@ -41,12 +41,58 @@ SEED_LOCK_AXES = (
|
|||||||
"composition",
|
"composition",
|
||||||
)
|
)
|
||||||
SEED_MODE_CHOICES = ["auto", "follow_main", "fixed", "random"]
|
SEED_MODE_CHOICES = ["auto", "follow_main", "fixed", "random"]
|
||||||
|
SEED_REROLL_GROUPS = {
|
||||||
|
"none": (),
|
||||||
|
"category": ("category",),
|
||||||
|
"subcategory": ("subcategory",),
|
||||||
|
"content": ("content",),
|
||||||
|
"person": ("person",),
|
||||||
|
"scene": ("scene",),
|
||||||
|
"pose": ("pose", "role"),
|
||||||
|
"role": ("role",),
|
||||||
|
"expression": ("expression",),
|
||||||
|
"composition": ("composition",),
|
||||||
|
"content_pose": ("content", "pose", "role"),
|
||||||
|
"scene_pose": ("scene", "pose", "role"),
|
||||||
|
}
|
||||||
|
SEED_REROLL_AXIS_CHOICES = list(SEED_REROLL_GROUPS.keys())
|
||||||
|
|
||||||
|
|
||||||
|
def _normal_key(value: Any) -> str:
|
||||||
|
return str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
||||||
|
|
||||||
|
|
||||||
def seed_mode_choices() -> list[str]:
|
def seed_mode_choices() -> list[str]:
|
||||||
return list(SEED_MODE_CHOICES)
|
return list(SEED_MODE_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_seed_mode(value: Any) -> str:
|
||||||
|
normalized = _normal_key(value)
|
||||||
|
aliases = {
|
||||||
|
"follow": "follow_main",
|
||||||
|
"followmain": "follow_main",
|
||||||
|
"follow_main_seed": "follow_main",
|
||||||
|
"main": "follow_main",
|
||||||
|
"main_seed": "follow_main",
|
||||||
|
}
|
||||||
|
normalized = aliases.get(normalized, normalized)
|
||||||
|
return normalized if normalized in SEED_MODE_CHOICES else "auto"
|
||||||
|
|
||||||
|
|
||||||
|
def seed_reroll_axis_choices() -> list[str]:
|
||||||
|
return list(SEED_REROLL_AXIS_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_reroll_axis(value: Any) -> str:
|
||||||
|
normalized = _normal_key(value)
|
||||||
|
aliases = {
|
||||||
|
"contentpose": "content_pose",
|
||||||
|
"scenepose": "scene_pose",
|
||||||
|
}
|
||||||
|
normalized = aliases.get(normalized, normalized)
|
||||||
|
return normalized if normalized in SEED_REROLL_GROUPS else "none"
|
||||||
|
|
||||||
|
|
||||||
def row_seed(seed: int, row_number: int, salt: int = 0) -> int:
|
def row_seed(seed: int, row_number: int, salt: int = 0) -> int:
|
||||||
return int(seed) + int(row_number) * 1009 + salt * 9176
|
return int(seed) + int(row_number) * 1009 + salt * 9176
|
||||||
|
|
||||||
@@ -74,7 +120,7 @@ def build_seed_config_json(
|
|||||||
rng = random.SystemRandom()
|
rng = random.SystemRandom()
|
||||||
|
|
||||||
def axis_seed(value: int, mode: str) -> int:
|
def axis_seed(value: int, mode: str) -> int:
|
||||||
mode = mode if mode in SEED_MODE_CHOICES else "auto"
|
mode = normalize_seed_mode(mode)
|
||||||
if mode == "auto":
|
if mode == "auto":
|
||||||
return int(value)
|
return int(value)
|
||||||
if mode == "random":
|
if mode == "random":
|
||||||
@@ -107,21 +153,7 @@ def build_seed_lock_config_json(
|
|||||||
) -> str:
|
) -> str:
|
||||||
base_seed = int(base_seed)
|
base_seed = int(base_seed)
|
||||||
reroll_seed = int(reroll_seed)
|
reroll_seed = int(reroll_seed)
|
||||||
reroll_groups = {
|
reroll = set(SEED_REROLL_GROUPS[normalize_reroll_axis(reroll_axis)])
|
||||||
"none": (),
|
|
||||||
"category": ("category",),
|
|
||||||
"subcategory": ("subcategory",),
|
|
||||||
"content": ("content",),
|
|
||||||
"person": ("person",),
|
|
||||||
"scene": ("scene",),
|
|
||||||
"pose": ("pose", "role"),
|
|
||||||
"role": ("role",),
|
|
||||||
"expression": ("expression",),
|
|
||||||
"composition": ("composition",),
|
|
||||||
"content_pose": ("content", "pose", "role"),
|
|
||||||
"scene_pose": ("scene", "pose", "role"),
|
|
||||||
}
|
|
||||||
reroll = set(reroll_groups.get(str(reroll_axis or "none"), ()))
|
|
||||||
config: dict[str, int] = {}
|
config: dict[str, int] = {}
|
||||||
for axis in SEED_LOCK_AXES:
|
for axis in SEED_LOCK_AXES:
|
||||||
config[f"{axis}_seed"] = reroll_seed if axis in reroll else base_seed
|
config[f"{axis}_seed"] = reroll_seed if axis in reroll else base_seed
|
||||||
@@ -157,9 +189,51 @@ def configured_axis_seed(seed_config: dict[str, int], axis: str) -> int | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def configured_seed_from_axes(
|
||||||
|
seed_config: str | dict[str, Any] | None,
|
||||||
|
axes: Iterable[str],
|
||||||
|
*,
|
||||||
|
extra_keys: Iterable[str] = (),
|
||||||
|
) -> int | None:
|
||||||
|
try:
|
||||||
|
parsed = parse_seed_config(seed_config)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
for axis in axes:
|
||||||
|
value = configured_axis_seed(parsed, axis)
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
for key in extra_keys:
|
||||||
|
value = parsed.get(str(key))
|
||||||
|
if value is not None and value >= 0:
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def axis_rng(seed_config: dict[str, int], axis: str, base_seed: int, row_number: int) -> random.Random:
|
def axis_rng(seed_config: dict[str, int], axis: str, base_seed: int, row_number: int) -> random.Random:
|
||||||
configured = configured_axis_seed(seed_config, axis)
|
configured = configured_axis_seed(seed_config, axis)
|
||||||
salt = SEED_AXIS_SALTS.get(axis, 0)
|
salt = SEED_AXIS_SALTS.get(axis, 0)
|
||||||
if configured is None:
|
if configured is None:
|
||||||
return random.Random(row_seed(base_seed, row_number, salt))
|
return random.Random(row_seed(base_seed, row_number, salt))
|
||||||
return random.Random(row_seed(configured, row_number, salt))
|
return random.Random(row_seed(configured, row_number, salt))
|
||||||
|
|
||||||
|
|
||||||
|
def axis_seed_trace(
|
||||||
|
seed_config: str | dict[str, Any] | None,
|
||||||
|
base_seed: int,
|
||||||
|
row_number: int,
|
||||||
|
axes: Iterable[str] = SEED_LOCK_AXES,
|
||||||
|
) -> dict[str, dict[str, int | str]]:
|
||||||
|
parsed = parse_seed_config(seed_config)
|
||||||
|
trace: dict[str, dict[str, int | str]] = {}
|
||||||
|
for axis in axes:
|
||||||
|
configured = configured_axis_seed(parsed, axis)
|
||||||
|
seed_value = int(configured) if configured is not None else int(base_seed)
|
||||||
|
source = "configured" if configured is not None else "main"
|
||||||
|
salt = SEED_AXIS_SALTS.get(axis, 0)
|
||||||
|
trace[axis] = {
|
||||||
|
"source": source,
|
||||||
|
"seed": seed_value,
|
||||||
|
"rng_seed": row_seed(seed_value, row_number, salt),
|
||||||
|
}
|
||||||
|
return trace
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def _clean(value: Any) -> str:
|
||||||
|
return " ".join(str(value or "").strip().split())
|
||||||
|
|
||||||
|
|
||||||
|
def softcore_cast_presence_phrase(
|
||||||
|
*,
|
||||||
|
same_cast: bool,
|
||||||
|
pov_labels: list[str] | tuple[str, ...] | None = None,
|
||||||
|
cast_label: str = "",
|
||||||
|
woman_label: str = "the woman",
|
||||||
|
) -> str:
|
||||||
|
pov_labels = [str(label) for label in (pov_labels or []) if str(label).strip()]
|
||||||
|
cast_label = _clean(cast_label)
|
||||||
|
woman_label = _clean(woman_label) or "the woman"
|
||||||
|
if same_cast and pov_labels:
|
||||||
|
return (
|
||||||
|
f"{woman_label} is framed from the POV participant's first-person creator camera, "
|
||||||
|
"with the POV participant implied by camera position or foreground body cues"
|
||||||
|
)
|
||||||
|
if same_cast:
|
||||||
|
visible_cast = cast_label or "the named cast"
|
||||||
|
verb = "share" if " and " in visible_cast or "," in visible_cast else "shares"
|
||||||
|
return f"{visible_cast} {verb} a styled creator-teaser frame"
|
||||||
|
return f"solo creator frame with {woman_label} as the only visible subject"
|
||||||
|
|
||||||
|
|
||||||
|
def softcore_caption_setup_phrase(*, same_cast: bool, target_auto: bool = False) -> str:
|
||||||
|
if same_cast:
|
||||||
|
return (
|
||||||
|
"The softcore side keeps the same adult cast together in a styled creator setup"
|
||||||
|
if target_auto
|
||||||
|
else "The same adult cast shares a styled creator setup"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
"The softcore side is a solo styled creator setup"
|
||||||
|
if target_auto
|
||||||
|
else "Solo styled creator setup"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def softcore_style_directive() -> str:
|
||||||
|
return f"Use {softcore_style_tag()}."
|
||||||
|
|
||||||
|
|
||||||
|
def softcore_style_tag() -> str:
|
||||||
|
return "seductive creator-shot teaser styling"
|
||||||
+1013
-11
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3819
-41
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user