Compare commits
321 Commits
main
..
84d4383f26
| Author | SHA1 | Date | |
|---|---|---|---|
| 84d4383f26 | |||
| b6c6df03ee | |||
| 50ded3c5fa | |||
| 12c5f73104 | |||
| 8c3f61ea6d | |||
| 885f136cf3 | |||
| 57c73279ef | |||
| 1d23ce32aa | |||
| c0985cddbe | |||
| cddc5d0a4d | |||
| 38979a79a1 | |||
| 39782ce843 | |||
| 5e218e2d33 | |||
| 5f602db06b | |||
| 83dfecc55b | |||
| 0e0a0c14b5 | |||
| a340def000 | |||
| b7381b9d51 | |||
| c95bb30a22 | |||
| b8d164a3da | |||
| 7e41613c1e | |||
| f8f2fb43df | |||
| caeafa0714 | |||
| 85c577024b | |||
| 665a23a7b2 | |||
| 337bbb10f1 | |||
| ff484aa27c | |||
| 4689cc7942 | |||
| 3832044256 | |||
| 5f4dd7d77f | |||
| f5ba07e340 | |||
| 284c6279e6 | |||
| 364c42103b | |||
| 49d130467b | |||
| 6a37c807bc | |||
| 2aafab03bd | |||
| 1e9794eed0 | |||
| 3467acbd6a | |||
| b8e15289ca | |||
| 03907439a4 | |||
| e028419e6d | |||
| 05f14cecc7 | |||
| 43a71c2353 | |||
| f937d3c109 | |||
| b41d140927 | |||
| f73eb72d68 | |||
| f855c7b022 | |||
| 2a29fcdfbb | |||
| 607c612196 | |||
| 8ff02a181b | |||
| 00e371e4b6 | |||
| 858fbe8d46 | |||
| d77e7631da | |||
| e96b9e9aae | |||
| 5a5d5dd6fe | |||
| 06525c42a3 | |||
| 3a09210f71 | |||
| 333f4752f6 | |||
| fae5423513 | |||
| d384cb8a46 | |||
| 742281f48f | |||
| 40ee843baf | |||
| 484fb40638 | |||
| a484783515 | |||
| 11b7c2acf9 | |||
| bb53967df4 | |||
| ef3b983712 | |||
| 0328e5ca3a | |||
| 54617e4702 | |||
| d937c219ee | |||
| f681fe2949 | |||
| 5acda5227c | |||
| d1af43bad2 | |||
| 4ca4653e7d | |||
| 3130942caf | |||
| faacfc8853 | |||
| 09d19a6f56 | |||
| debb6d6f38 | |||
| e434bd66ad | |||
| 509960a699 | |||
| ab8abc07e6 | |||
| 14f984a629 | |||
| 8d58bfdf6a | |||
| b8d8066fdb | |||
| 78e39734b5 | |||
| 4c8edc0d3e | |||
| e2c4ecb853 | |||
| 3c54bb4bbe | |||
| 29efb954fb | |||
| 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 | |||
| f1567118b4 | |||
| 2605fae3eb | |||
| 8fc3abc504 | |||
| d7caf1c270 | |||
| b38b27acfd | |||
| 0ccb87799b | |||
| 09fc31f078 | |||
| 5ec17df1a4 | |||
| 176d4c9257 | |||
| 8398a97cdf | |||
| 28612f9d00 | |||
| 2c978c7eab | |||
| 00139d0cd9 | |||
| 6abd17b165 | |||
| 2b221463ee | |||
| 09eaafc8f6 | |||
| a5b648eb98 | |||
| 58abbaa347 | |||
| ddf72a87dd | |||
| a7e1a37ad8 | |||
| f7164480df | |||
| d31d513ec3 | |||
| c076b22b75 | |||
| 55fec890a5 | |||
| b46b709e8a | |||
| 3c1f6784c1 | |||
| 23bcb1b526 | |||
| 58ddda82d7 | |||
| 3d9dbdc95d | |||
| e5822e42f8 | |||
| d9275f5f0c | |||
| 70a8698cbe | |||
| e9cc75bd5f | |||
| 3f251a6bb7 | |||
| b3fce97efd | |||
| 20c69b6feb | |||
| 9884b6f6e7 | |||
| 972c8f14b6 | |||
| 049f2c6e87 | |||
| 61535cc60d | |||
| 9ca2320df2 | |||
| 7f808be997 | |||
| d4d3be5789 | |||
| 1cc65e35b5 | |||
| 132d457bf7 | |||
| 0eada863d8 | |||
| ab2a13ecde | |||
| cfe11a4634 | |||
| c0c2fb2b40 | |||
| 7d112c0f98 | |||
| dfdfff953b | |||
| de1d23fb37 | |||
| dc94b1c4c1 | |||
| 2d3d668359 | |||
| 5ab2433ca7 | |||
| 21da2949c6 | |||
| 36ce394462 | |||
| 5efa073bfb | |||
| 64887a2750 | |||
| a128b2dc9a | |||
| 4c45d96472 | |||
| b54b8b9421 | |||
| 2165e9fc16 | |||
| 6a3f88ef59 | |||
| 50d0ffa7e3 | |||
| 5675536009 | |||
| 65574222b2 | |||
| 4c31553409 | |||
| f3f9929df5 | |||
| fef2bf6d81 | |||
| 6abcccbae1 | |||
| f552f76c1a | |||
| bc5ec35ef7 | |||
| 30b5280da1 | |||
| ef8b7f5b89 | |||
| c8c95db835 | |||
| e56e7173ea | |||
| d01de98516 | |||
| efe13beb79 | |||
| 49fe509aa7 | |||
| e6937d96ac | |||
| 029ece173e | |||
| 9b9b0cbb4c | |||
| b7939a4748 | |||
| e1ec8bd823 | |||
| 8bff345cf7 | |||
| 1ad2015308 | |||
| aeea75c485 | |||
| 7a1d1dcac0 | |||
| dcddfe5d61 | |||
| 3ebbb09d63 | |||
| ee62e2215d | |||
| 04ee754f68 | |||
| 86a8f6167a | |||
| 4646f97ee7 | |||
| 0e7cf60fcb | |||
| f27ba23a62 | |||
| 3cbded3f45 | |||
| 2f7c359fab | |||
| 8668dfec9d | |||
| 0e49aed8ac | |||
| f6d6dfffb4 | |||
| c08af2c14a | |||
| ce90fb7593 | |||
| 1c661b3c9d | |||
| 0b9ee3b8b1 | |||
| 6c5a529e29 | |||
| 659a730169 | |||
| 031223255d | |||
| 92469daf03 | |||
| a4a8a7a28e | |||
| b82cf3fbbf | |||
| 97c49fffed | |||
| 1a98fdb9f2 | |||
| 5c5120a1f9 |
@@ -2,3 +2,4 @@ __pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.sxcp_eval/
|
||||
|
||||
+46
-72
@@ -1,81 +1,55 @@
|
||||
# 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.
|
||||
- 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.
|
||||
## Current Baseline
|
||||
|
||||
## 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`
|
||||
- `nude`
|
||||
- `explicit`
|
||||
- `hardcore`
|
||||
- A generated prompt shows concrete noise, contradiction, or hidden logic drift.
|
||||
- A workflow action is awkward in ComfyUI and needs a better node surface.
|
||||
- A new metadata field, node, category family, formatter route, or location
|
||||
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
|
||||
the pose/content pools and negative prompts.
|
||||
## Concrete Next Work
|
||||
|
||||
2. Anatomy clarity axis
|
||||
|
||||
Add a controlled axis for visual clarity:
|
||||
|
||||
- full-body view
|
||||
- hips-focused view
|
||||
- genital-contact view
|
||||
- face-and-body view
|
||||
- mirror view
|
||||
|
||||
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.
|
||||
1. Add route-level smoke fixtures only for observed generated edge cases or new
|
||||
metadata fields that affect Krea2, SDXL, or caption output.
|
||||
2. Extend `scene_camera_adapters.py` one location family at a time, after the
|
||||
actual generated prompts show the location needs camera-aware wording.
|
||||
3. Add characteristic side nodes only when repeated manual slot fields become
|
||||
workflow friction.
|
||||
4. Tune hardcore, softcore, SDXL, or caption wording only from real output
|
||||
examples, not from speculative prompt rules.
|
||||
5. When adding a node/path, update the route map and audit coverage in the same
|
||||
change so organization stays discoverable.
|
||||
|
||||
@@ -22,6 +22,7 @@ The node is registered as:
|
||||
- `prompt_builder / SxCP Cast Control`
|
||||
- `prompt_builder / SxCP Cast Bias`
|
||||
- `prompt_builder / SxCP Generation Profile`
|
||||
- `prompt_builder / SxCP Style Pool`
|
||||
- `prompt_builder / SxCP Ethnicity List`
|
||||
- `prompt_builder / SxCP Hair Length`
|
||||
- `prompt_builder / SxCP Hair Color`
|
||||
@@ -38,6 +39,23 @@ The node is registered as:
|
||||
- `prompt_builder / SxCP Krea2 Formatter`
|
||||
- `prompt_builder / SxCP Insta/OF Options`
|
||||
- `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:
|
||||
|
||||
@@ -64,14 +82,23 @@ node. For cleaner workflows, use the split nodes:
|
||||
`men_weights=0.5,0.3` means 50% no man and 30% one man.
|
||||
- `SxCP Location Pool` outputs `location_config`. `replace` uses only the
|
||||
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
|
||||
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
|
||||
`composition_config`. Themes such as `classical_library`,
|
||||
`semi_public_affair`, `hotel_corridor`, `parking_garage`, and
|
||||
`theater_backstage` keep scene and framing compatible.
|
||||
- `SxCP Style Pool` outputs `style_config` for visual rendering style only.
|
||||
It can force realistic/photo/cinematic/comic output independently from
|
||||
category, action, pose, location, and camera. The previous colored-pencil
|
||||
comic wording is available as the `comic_pinup_colored_pencil` preset instead
|
||||
of being baked into hardcore pose prompts.
|
||||
- `SxCP Generation Profile` outputs `generation_profile` for common behavior
|
||||
presets such as casual-clean, evocative-softcore, hardcore-intense,
|
||||
Krea2-friendly, or Flux-original. Its clothing and pose overrides can be
|
||||
@@ -94,9 +121,33 @@ The practical compact workflow is:
|
||||
`Category Preset` + `Cast Control` + `Generation Profile` + optional
|
||||
`Advanced Filters`, `Seed Locker` or `Seed Control`, `Camera Control` or
|
||||
`Camera Orbit Control`, `Location Theme` or `Location Pool` + `Composition Pool`,
|
||||
`Woman Slot` / `Man Slot`, and `Character Profile`
|
||||
`Style Pool`, `Woman Slot` / `Man Slot`, and `Character Profile`
|
||||
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`.
|
||||
|
||||
Each layer can stay light on the main chain and take optional side-node inputs:
|
||||
`SxCP Scene Layer Seed Options`, `SxCP Scene Cast Options`,
|
||||
`SxCP Scene Character Options`, `SxCP Scene Wardrobe Options`,
|
||||
`SxCP Scene Location Layout Options`, `SxCP Scene Set Dressing Options`,
|
||||
`SxCP Scene Blocking Options`, `SxCP Scene Action Options`,
|
||||
`SxCP Scene Performance Options`, `SxCP Scene Camera Options`,
|
||||
`SxCP Scene Composition Options`, `SxCP Scene Lighting Options`, and
|
||||
`SxCP Scene Branch Options`. These side nodes are chainable and only override
|
||||
the layer they are connected to.
|
||||
|
||||
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
|
||||
`examples/default_task_lanes_workflow.json`. It is laid out by task instead of
|
||||
as one long chain:
|
||||
@@ -109,6 +160,20 @@ as one long chain:
|
||||
manually into either generation lane, but they are not part of the default
|
||||
main path.
|
||||
|
||||
A dedicated v2 scene-chain Insta/OF branching demo is available at
|
||||
`examples/scene_chain_insta_of_branching_workflow.json`.
|
||||
|
||||
A proposed adapter-style v2 layout is available at
|
||||
`examples/scene_chain_adapter_layout_workflow.json`. It keeps the main scene
|
||||
chain in one center lane and parks layer-specific option nodes beside the layer
|
||||
they override.
|
||||
|
||||
A no-freeform workspace lounge Insta/OF branch demo is available at
|
||||
`examples/scene_chain_workspace_lounge_insta_of_workflow.json`. It uses the
|
||||
`workspace_lounge` location theme with camera orbit control, so the coworking
|
||||
layout text adapts to camera position while the softcore and hardcore prompts
|
||||
stay split.
|
||||
|
||||
## Loop Nodes
|
||||
|
||||
`SxCP For Loop Start` and `SxCP For Loop End` provide a lightweight replacement
|
||||
@@ -126,10 +191,10 @@ Basic loop wiring:
|
||||
5. After the loop finishes, use `For Loop End.collected` as the combined output.
|
||||
|
||||
`For Loop Start.index` is 1-based so it can be wired directly into prompt-builder
|
||||
`row_number` inputs. `For Loop Start.skip` skips the first N iterations while
|
||||
keeping the remaining row numbers stable. For example, `total=10` and `skip=1`
|
||||
runs indexes `2..10`; `skip=5` runs indexes `6..10`. This is useful when you
|
||||
want to resume a loop without changing index-derived seeds or row numbers.
|
||||
`row_number` inputs. `For Loop Start.schedule` is an optional input for choosing
|
||||
which indexes run while keeping row numbers stable. Omit it to run `1..total`,
|
||||
connect a list such as `[2, 5, 8]`, or connect text such as `2,5,8` or `2-8`.
|
||||
Indexes outside `1..total` are ignored.
|
||||
|
||||
`collection_mode` controls how values are stored:
|
||||
|
||||
@@ -331,11 +396,11 @@ prompt result. Manual fields and explicitly fixed per-axis or character-slot
|
||||
seeds still override the global seed for those parts.
|
||||
|
||||
`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
|
||||
visible seed value is materialized before the workflow queues, and that exact
|
||||
value is used for the queued prompt. The mode returns to `random` after queueing
|
||||
so the next run can reroll. Use `Lock Random Seeds Now` on the node when you want
|
||||
to convert the current random axes into fixed reusable seeds.
|
||||
builder's optional `seed_config` input. It also outputs a `summary` string with
|
||||
the resolved value for every axis. When an axis is set to `random`, the widget
|
||||
can stay at `-1`, but the emitted `seed_config` and `summary` contain the
|
||||
concrete seed used for that queued prompt. Use `Lock Random Seeds Now` on the
|
||||
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
|
||||
you like, choose one `reroll_axis`, and connect its `seed_config`. All other
|
||||
@@ -413,13 +478,42 @@ 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
|
||||
generic Qwen camera views do not add `phone hidden` or other phone wording.
|
||||
|
||||
For coworking-style locations, the prompt builder also uses the translated
|
||||
camera geometry to add a location-aware framing sentence. It currently targets
|
||||
`coworking lounge`, `business cafe`, and empty office scenes: front/side/back
|
||||
views, zoom, and elevation change which desks, windows, laptop tables, glass
|
||||
partitions, counters, or office rows are kept visible. In male-POV setups this
|
||||
becomes a first-person spatial description and the external camera sentence is
|
||||
suppressed.
|
||||
For camera-aware locations, the prompt builder also uses the translated camera
|
||||
geometry to add a location-aware framing sentence. It currently has scene
|
||||
profiles for coworking/business-office spaces, classical library/book-stack
|
||||
spaces, and semi-public repeating-structure locations such as hotel corridors,
|
||||
parking garages, archives, laundromats, station lockers, backstage halls, wine
|
||||
cellars, nightclub back halls, and restaurant booths. Front/side/back views,
|
||||
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
|
||||
comma-tag SDXL/Pony-style prompts. Connect `metadata_json` when possible so
|
||||
character, camera, outfit, and action metadata stay available to the tag route.
|
||||
|
||||
SDXL formatter controls:
|
||||
|
||||
- `formatter_profile`: `manual_controls` keeps `style_preset` and
|
||||
`quality_preset` authoritative. `pony_flat_vector`, `sdxl_photo`, and
|
||||
`flat_vector` apply coherent formatter defaults.
|
||||
- `style_preset`: positive style anchor such as `flat_vector_pony`,
|
||||
`flat_vector`, or `photographic`.
|
||||
- `quality_preset`: quality/score tail such as `pony_high` or `sdxl_high`.
|
||||
- `trigger` and `prepend_trigger_to_prompt`: explicit model/LoRA trigger
|
||||
placement for SDXL-style workflows.
|
||||
- `custom_style` and `custom_quality`: override the selected preset text.
|
||||
|
||||
`SxCP Caption Naturalizer` rewrites tag-like captions or labeled prompts into
|
||||
more natural language. Connect the prompt builder's `metadata_json` output to
|
||||
@@ -429,13 +523,20 @@ cleanup.
|
||||
|
||||
When connected to `SxCP Insta/OF Prompt Pair` metadata, the naturalizer emits a
|
||||
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
|
||||
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:
|
||||
|
||||
- `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
|
||||
authoritative; `training_concise`, `training_dense`, and `browsing` apply
|
||||
preset caption behavior.
|
||||
- `detail_level`: `concise`, `balanced`, or `dense`.
|
||||
- `style_policy`: `drop_style_tail` removes old fixed style tails; `keep_style_terms`
|
||||
keeps style descriptions in the rewritten text.
|
||||
@@ -645,7 +746,7 @@ Example:
|
||||
"slug": "casual_clothes",
|
||||
"subject_type": "woman",
|
||||
"item_label": "Clothing",
|
||||
"style": "tasteful adult fashion-editorial coloured-pencil comic illustration",
|
||||
"style": "realistic casual social-feed photo with everyday styling",
|
||||
"subcategories": [
|
||||
{
|
||||
"name": "Streetwear",
|
||||
@@ -847,10 +948,12 @@ axis has its own mode plus seed value:
|
||||
- `follow_main`: always follows the final generator's main `seed` input and
|
||||
ignores 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
|
||||
visible concrete seed and switches those axes to `fixed`.
|
||||
The `summary` output lists the resolved value for every axis, including random
|
||||
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:
|
||||
|
||||
|
||||
+159
-3156
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
# Atlas Refine Baseline Deck Analysis
|
||||
|
||||
Date: 2026-07-01
|
||||
|
||||
Folder:
|
||||
`/media/unraid/comfyui/output/CodexMCP-Atlas-Refine`
|
||||
|
||||
Subject id:
|
||||
`atlas_refine_same_woman_001`
|
||||
|
||||
## Folder State
|
||||
|
||||
The folder contains 16 complete `.png` / `.txt` atlas prompt pairs.
|
||||
|
||||
Coverage report:
|
||||
|
||||
- 10 clean `baseline_only` entries
|
||||
- 6 `needs_prompt_cleanup` entries
|
||||
- 0 sidecar prompt variants
|
||||
- 0 seedable catalog cue candidates
|
||||
- 0 missing image/text pairs
|
||||
|
||||
The deck is useful as a controlled same-woman baseline set, not yet as a
|
||||
seed/cue system. Reviewed sidecar variants still need to be authored from
|
||||
actual prompt/image evidence.
|
||||
|
||||
## Prompt Cleanup Queue
|
||||
|
||||
These baseline prompts still contain positive-channel negative wording through
|
||||
`without` and should be rewritten before visual scoring or seed promotion:
|
||||
|
||||
- `pov_cowgirl_alt_low_squat_penetration`
|
||||
- `pov_cowgirl_frontal_straddle_penetration`
|
||||
- `pov_ejaculation_aftermath_open_thigh_candidate`
|
||||
- `pov_handjob_upright_centered`
|
||||
- `pov_reverse_cowgirl_alt_upright_back_facing_penetration`
|
||||
- `pov_reverse_cowgirl_back_facing_penetration`
|
||||
|
||||
## Visual Deck Read
|
||||
|
||||
The deck is strong for same-subject comparison:
|
||||
|
||||
- the woman identity is stable across poses;
|
||||
- the coworking lounge style is consistent;
|
||||
- desk rows, laptops, chair wheels, plants, glass partitions, and warm window
|
||||
depth are repeated enough to support workspace-continuity scoring;
|
||||
- most poses place the action as the primary foreground subject.
|
||||
|
||||
Current limitations:
|
||||
|
||||
- all entries are single baseline images, so they do not test seedable
|
||||
alternatives yet;
|
||||
- several prompts still use cleanup-needed wording;
|
||||
- clothing/control is not represented across most poses;
|
||||
- workspace interaction is mostly background context, not varied physical use
|
||||
of the lounge furniture;
|
||||
- exact top-view oral remains weak under text-only prompting even after the
|
||||
second MCP batch.
|
||||
|
||||
## Next Evidence Priorities
|
||||
|
||||
1. Clean the six `without` prompts before using them for cue seeds.
|
||||
2. Build prompt-variant sidecars only from tested cues, not invented wording.
|
||||
3. For each pose family, collect same-subject fixed-seed variants before
|
||||
touching generator defaults.
|
||||
4. Score scene/pose height from background cues: floor plane, desk height,
|
||||
chair wheels, table bases, and whether the viewer/partner appears standing,
|
||||
seated, kneeling, reclined, or supported by furniture.
|
||||
5. Keep clothing restore as woman-owned visible detail, and only for garments
|
||||
that the pose crop can actually show.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# Blowjob Top-Down Vertical Shaft A/B Pass
|
||||
|
||||
Date: 2026-07-01
|
||||
Batch: `ab_batches/blowjob_top_down_vertical_shaft_axis_batch.json`
|
||||
Results: `ab_batches/blowjob_top_down_vertical_shaft_axis_results.json`
|
||||
Sampler seed: `238365845574312`
|
||||
Pose target: `pov_blowjob_top_down_vertical_shaft`
|
||||
|
||||
## Reference Read
|
||||
|
||||
Atlas examples `27_blowjob_top_view.png` and `2_blowjob_top_view.png` read flatter and more vertical than most generator outputs: the viewer appears standing or high over the woman, the floor/surface plane dominates, and the woman is directly below the camera axis. The generated coworking baseline already has useful office anchors, but it still reads like an elevated forward-looking POV rather than a pure top-down/nadir shot.
|
||||
|
||||
## Strongest Variants
|
||||
|
||||
- `axis_mouth_directly_below_torso` -> `/media/unraid/comfyui/output/agent_bridge/img_75cd8e71ddad45f4a4b2aa9e00ea6127.png`
|
||||
- Best overall single-seed improvement. Contact is preserved, mouth is centered below torso, both hands hold the base, and the office/floor read remains coherent.
|
||||
- Still not as vertically flat as atlas `27` or `2`.
|
||||
- `axis_floor_plane_priority` -> `/media/unraid/comfyui/output/agent_bridge/img_867f5eea66354fb4beb5df586e56bfbc.png`
|
||||
- Good floor-plane/workspace read, contact preserved, strong centered column.
|
||||
- Slightly less direct mouth alignment than the best candidate, but useful wording.
|
||||
- `axis_wide_floor_coworking_rows` -> `/media/unraid/comfyui/output/agent_bridge/img_696bb9f3163049f5b696316db97b46f2.png`
|
||||
- Good workspace continuity and repeated desk/chair floor grid. Contact preserved.
|
||||
- Still camera-forward rather than true nadir.
|
||||
- `axis_chair_wheel_floor` -> `/media/unraid/comfyui/output/agent_bridge/img_5265517ff9544c149277a8711461da17.png`
|
||||
- Contact preserved with strong chair-wheel/floor anchors.
|
||||
- Similar axis to baseline, but cleaner workspace evidence.
|
||||
- `axis_eye_contact_vertical` -> `/media/unraid/comfyui/output/agent_bridge/img_acfdcfda262849eea33dd5b31985751c.png`
|
||||
- Useful expression/eye-control probe. Preserves contact and subject look.
|
||||
- Does not solve the flat vertical camera read.
|
||||
- `axis_clothed_top_visible` -> `/media/unraid/comfyui/output/agent_bridge/img_13516b4bed1a4a69a8542b446822836e.png`
|
||||
- Woman-owned clothing worked: tank top stayed on the woman and contact remained.
|
||||
- Useful for later clothing-restore rules, not the main vertical-axis fix.
|
||||
|
||||
## Weak Or Broken Variants
|
||||
|
||||
- `axis_standing_feet_close` broke oral contact.
|
||||
- `axis_carpet_seam_centerline` produced a centerline artifact and broke contact.
|
||||
- `axis_glass_partition_floor` broke contact.
|
||||
- `axis_knees_visible_below_head` broke contact.
|
||||
- `axis_compact_anatomy` broke contact and did not improve axis.
|
||||
- `axis_bralette_open_shirt` preserved woman-owned clothing but broke oral contact.
|
||||
- `axis_low_lounge_table` preserved workspace interaction but broke oral contact.
|
||||
- `axis_tight_vertical_crop` kept contact but made anatomy too tall/large and did not improve atlas verticality enough.
|
||||
|
||||
## Wording Takeaways
|
||||
|
||||
- Useful wording:
|
||||
- `mouth directly below the viewer's torso`
|
||||
- `floor-plane-priority`
|
||||
- `rows of desk legs, chair wheels, table corners, and carpet seams extend across the floor`
|
||||
- `chair wheels, caster bases, desk legs, and carpet texture surround the kneeling woman from above`
|
||||
- `face tilted upward, dark almond eyes looking up into the camera`
|
||||
- woman-owned clothing: `the woman wears a fitted white ribbed tank top`
|
||||
- Risky wording:
|
||||
- `centerline` can create literal artifacts.
|
||||
- `glass partition floor rail` competes with the action.
|
||||
- `full kneeling posture visible` pulls the mouth away from contact.
|
||||
- `foreshortened compact cylinder` did not improve anatomy; it weakened contact.
|
||||
- Low table / desk edge interactions can improve workspace but often move the action away from the mouth.
|
||||
|
||||
## Next Batch
|
||||
|
||||
Do not promote to generator/catalog from this single-seed run. The next batch should narrow around the top candidates:
|
||||
|
||||
- Keep `axis_mouth_directly_below_torso` as the center prompt.
|
||||
- Hybridize it with `floor-plane-priority`, `wide coworking rows`, and `chair-wheel floor` anchors.
|
||||
- Add 12-16 variants that push more verticality using concrete camera/surface language while preserving the exact mouth-contact hierarchy.
|
||||
- Run at least two sampler seeds before sidecar promotion; use more if the first two disagree.
|
||||
@@ -0,0 +1,428 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_out": "sxcp_eval_out",
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"subject_id": "atlas_refine_same_woman_001",
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft",
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"source_prompt_sha256": "1d0b95d9865d1a502fb91bc856b3ff4baf00da90248c47d45631fb512f58a463",
|
||||
"probes": [
|
||||
{
|
||||
"id": "baseline_top_down_vertical_shaft",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir-angle standing male POV top-view oral position. viewer looks almost straight down from his torso toward the floor. nearby carpet/floor plane dominates the image. viewer abdomen, shorts, thighs, and feet frame the lower foreground. large penis is a short centered vertical column. the woman kneels directly below the viewer between his feet. her mouth seals around the centered large penis. one hand wraps the base. hair crown, forehead, shoulders, hands, and knees are visible from above. desk legs, chair wheels, carpet texture, and floor seams act as top-down office anchors. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "baseline_nadir_standing",
|
||||
"workspace_surface": "coworking_carpet_floor",
|
||||
"body_angle": "woman_kneels_below_viewer",
|
||||
"hand_position": "one_hand_base"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 0
|
||||
},
|
||||
"notes": "control prompt from same-subject atlas refine deck"
|
||||
},
|
||||
{
|
||||
"id": "axis_floor_fills_frame",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. True nadir standing male POV oral position. the camera points straight down from the viewer's abdomen to the carpet. carpet texture and floor seams fill the frame behind the woman. viewer shorts, abdomen, thighs, knees, and bare feet form a tight lower border. the large penis rises as a compact vertical column in the exact center. the woman kneels directly between the viewer's feet with both knees on the carpet. her hair crown, forehead, shoulders, hands, and knees read from above. her mouth seals around the centered large penis while her right hand wraps the base. desk legs and chair wheels appear as top-down office marks around the carpet plane. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "true_nadir_floor_fills_frame",
|
||||
"workspace_surface": "carpet_floor_plane",
|
||||
"body_angle": "kneeling_between_feet",
|
||||
"hand_position": "right_hand_base"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 101
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_floor_fills_frame"
|
||||
},
|
||||
"notes": "maximize floor-plane dominance and near-vertical camera"
|
||||
},
|
||||
{
|
||||
"id": "axis_standing_feet_close",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position from a steep overhead angle. viewer stands on carpet with both bare feet close to the woman's knees. viewer abdomen and open shorts sit along the lower edge, thighs descend to the lower corners. the large penis stands upright as a centered foreshortened column. the woman kneels low under the viewer's torso, directly below the camera axis. her face tilts up, dark almond eyes visible, mouth sealed around the centered large penis. her left hand wraps the base while the other hand rests on the viewer's thigh. carpet seams, chair wheels, and desk legs surround the kneeling pose from above. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "standing_feet_close",
|
||||
"expression_eye_detail": "eyes_visible_upward",
|
||||
"hand_position": "one_hand_base_one_hand_thigh",
|
||||
"workspace_surface": "carpet_with_chair_wheels"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 102
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_standing_feet_close"
|
||||
},
|
||||
"notes": "tests explicit standing feet and upward eye contact"
|
||||
},
|
||||
{
|
||||
"id": "axis_desk_leg_grid",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep top-down standing male POV oral position inside a coworking desk row. the viewer looks down past his abdomen, open shorts, thighs, and feet. the floor plane dominates the entire image. black desk legs form straight vertical rods around the woman's shoulders and knees. rolling chair bases and carpet seams mark the overhead office geometry. the large penis is centered as a compact vertical cylinder. the woman kneels directly below the viewer, her head centered between his feet, mouth sealed around the tip, both hands stacked at the base. hair crown, brow, shoulders, hands, and knees stay readable from above. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "steep_top_down",
|
||||
"workspace_surface": "desk_leg_grid",
|
||||
"hand_position": "both_hands_stacked_base",
|
||||
"body_angle": "head_centered_between_feet"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 103
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_desk_leg_grid"
|
||||
},
|
||||
"notes": "workspace anchors should prove top-down geometry"
|
||||
},
|
||||
{
|
||||
"id": "axis_carpet_seam_centerline",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position with a strong carpet seam centerline. viewer abdomen, open shorts, thighs, and feet frame the lower edge. a long carpet seam runs away from the viewer through the center behind the woman's head. the large penis rises from the lower center as a compact vertical column aligned with that seam. the woman kneels directly below, knees on both sides of the seam, mouth sealed around the centered large penis. one hand wraps the base, the other hand rests flat on the carpet. her hair crown, eyes, shoulders, hands, and knees are seen from above. desk legs and chair wheels sit along the side edges. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "nadir_centerline",
|
||||
"workspace_surface": "carpet_seam_centerline",
|
||||
"hand_position": "one_hand_base_one_hand_floor",
|
||||
"expression_eye_detail": "eyes_visible"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 104
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_carpet_seam_centerline"
|
||||
},
|
||||
"notes": "tests centerline depth cue and avoids horizon wording"
|
||||
},
|
||||
{
|
||||
"id": "axis_chair_wheel_floor",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position beside rolling office chairs. the camera points down from the viewer's torso to the carpet. viewer abdomen, shorts, thighs, knees, and feet make the lower foreground border. chair wheels, caster bases, desk legs, and carpet texture surround the kneeling woman from above. the large penis is a centered vertical cylinder rising from the lower middle. the woman kneels below the viewer between his feet with shoulders tucked under his torso line. her mouth seals around the centered large penis. both hands wrap low at the base. her hair crown and dark eyes angle upward toward the camera. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "standing_top_view",
|
||||
"workspace_surface": "chair_wheel_floor",
|
||||
"hand_position": "both_hands_low_base",
|
||||
"expression_eye_detail": "dark_eyes_upward"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 105
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_chair_wheel_floor"
|
||||
},
|
||||
"notes": "tests chair wheel overhead anchors"
|
||||
},
|
||||
{
|
||||
"id": "axis_under_table_edge",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep standing male POV top-view oral position beside a coworking desk edge. viewer stands at the desk edge looking down past his abdomen, shorts, thighs, and feet. the wooden tabletop corner appears as a flat rectangle along the upper side of the frame. desk legs descend beside the woman's shoulders. carpet fills the space beneath her knees. the large penis appears as a centered foreshortened vertical column. the woman kneels below the viewer, mouth sealed around the centered large penis, both hands clasping the base. hair crown, forehead, shoulders, hands, and knees are visible from above. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "steep_standing",
|
||||
"workspace_surface": "desk_edge_topdown",
|
||||
"hand_position": "both_hands_clasp_base",
|
||||
"body_angle": "knees_under_desk_edge"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 106
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_under_table_edge"
|
||||
},
|
||||
"notes": "tests workspace interaction through a visible desk edge"
|
||||
},
|
||||
{
|
||||
"id": "axis_glass_partition_floor",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. True top-down standing male POV oral position beside a glass partition base. the viewer looks straight down from his abdomen toward the carpet. viewer shorts, thighs, knees, and bare feet frame the lower foreground. the glass partition seam and black floor rail run along one side of the carpet plane. the large penis rises as a compact vertical column at the center. the woman kneels directly below the viewer between his feet with her knees parallel to the rail. her mouth seals around the centered large penis. one hand wraps the base and one hand touches the floor rail side of the carpet. her eyes glance upward from below the hairline. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "true_top_down",
|
||||
"workspace_surface": "glass_partition_floor_rail",
|
||||
"hand_position": "base_and_floor",
|
||||
"expression_eye_detail": "upward_glance"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 107
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_glass_partition_floor"
|
||||
},
|
||||
"notes": "tests glass partition as top-down side anchor"
|
||||
},
|
||||
{
|
||||
"id": "axis_mouth_directly_below_torso",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position with the woman's mouth directly below the viewer's torso. viewer abdomen and open shorts occupy the lower edge, thighs and feet bracket the lower corners. the large penis rises from the lower center as a compact vertical column. the woman's face sits centered directly under the shaft, mouth sealed around it, hair crown and forehead close to the middle of the frame. both shoulders slope downward toward her knees on the carpet. both hands hold the base with fingers visible from above. surrounding desk legs, chair wheels, carpet seams, and floor texture stay beside the bodies. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "nadir_direct_mouth_alignment",
|
||||
"contact_depth": "mouth_directly_below_torso",
|
||||
"hand_position": "both_hands_base_visible",
|
||||
"workspace_surface": "surrounding_floor_anchors"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 108
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_mouth_directly_below_torso"
|
||||
},
|
||||
"notes": "tests contact alignment rather than background depth"
|
||||
},
|
||||
{
|
||||
"id": "axis_knees_visible_below_head",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position with the woman's full kneeling posture visible from above. viewer abdomen, shorts, thighs, and bare feet frame the lower foreground. her head is centered between the viewer's feet, her shoulders sit below the shaft line, and her knees appear behind her elbows on the carpet. the large penis is a compact centered vertical column. her mouth seals around the centered large penis. one hand wraps the base while the other hand rests on her own thigh. carpet weave, floor seams, chair wheels, and desk legs create a top-down office grid around her knees. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "top_view_full_kneel",
|
||||
"body_angle": "knees_visible_below_head",
|
||||
"hand_position": "base_and_own_thigh",
|
||||
"workspace_surface": "office_grid_floor"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 109
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_knees_visible_below_head"
|
||||
},
|
||||
"notes": "tests full kneeling silhouette"
|
||||
},
|
||||
{
|
||||
"id": "axis_compact_anatomy",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Near-vertical standing male POV oral position. viewer looks down from his abdomen toward the carpet between his feet. viewer shorts, thighs, knees, and bare feet anchor the lower frame. the large penis appears foreshortened by the top-down angle as a compact centered cylinder with rounded tip at the woman's lips. the woman kneels directly below the viewer, shoulders narrow under the camera, mouth sealed around the centered tip. her right hand grips the base, left hand rests on the viewer's thigh. carpet seams, desk legs, rolling chair bases, and floor texture remain flat behind her. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "near_vertical",
|
||||
"anatomy_shape_detail": "foreshortened_compact_cylinder",
|
||||
"hand_position": "base_and_viewer_thigh",
|
||||
"workspace_surface": "flat_floor_background"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 110
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_compact_anatomy"
|
||||
},
|
||||
"notes": "tests anatomy length control for top-down angle"
|
||||
},
|
||||
{
|
||||
"id": "axis_eye_contact_vertical",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep overhead standing male POV oral position. viewer looks down past abdomen, open shorts, thighs, and feet onto the carpet. the woman kneels directly between his feet, face tilted upward, dark almond eyes looking up into the camera from below her brow. the large penis is a centered compact vertical column leading from the lower edge to her mouth. her lips seal around the centered tip. both hands wrap the base with long fingers visible from above. carpet texture fills the frame, desk legs and chair wheels appear as overhead office anchors around her shoulders and knees. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "steep_overhead",
|
||||
"expression_eye_detail": "direct_upward_eye_contact",
|
||||
"hand_position": "both_hands_base",
|
||||
"workspace_surface": "carpet_texture_fill"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 111
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_eye_contact_vertical"
|
||||
},
|
||||
"notes": "tests eyes while preserving vertical axis"
|
||||
},
|
||||
{
|
||||
"id": "axis_soft_expression",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. True nadir standing male POV top-view oral position. viewer abdomen, open shorts, thighs, knees, and bare feet create the lower foreground frame. carpet plane and desk-leg shadows fill the rest of the image. the large penis rises at the exact center as a compact vertical column. the woman kneels directly below the viewer, mouth sealed around the centered large penis, eyes lifted upward with a calm focused expression. her hair crown, forehead, lashes, shoulders, hands, and knees are visible from above. one hand wraps the base, one hand rests on the carpet beside her knee. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "true_nadir",
|
||||
"expression_eye_detail": "calm_focused_upward_expression",
|
||||
"hand_position": "base_and_carpet",
|
||||
"workspace_surface": "desk_leg_shadows_on_carpet"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 112
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_soft_expression"
|
||||
},
|
||||
"notes": "tests expression control under strict axis"
|
||||
},
|
||||
{
|
||||
"id": "axis_clothed_top_visible",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position from directly above. viewer abdomen, open shorts, thighs, and bare feet frame the lower foreground. the woman kneels between his feet on the coworking carpet. the woman wears a fitted white ribbed tank top visible across her shoulders and chest. the large penis is a centered foreshortened vertical column leading to her mouth. her mouth seals around the centered large penis, one hand wrapped at the base and one hand on her own chest. hair crown, brow, shoulders, hands, tank top straps, and knees read from above. desk legs, chair wheels, carpet seams, and floor texture surround the kneeling pose. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "directly_above",
|
||||
"clothing_visibility": "woman_white_tank_top_visible",
|
||||
"hand_position": "base_and_own_chest",
|
||||
"workspace_surface": "coworking_carpet"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 113
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_clothed_top_visible"
|
||||
},
|
||||
"notes": "tests woman-owned clothing visibility"
|
||||
},
|
||||
{
|
||||
"id": "axis_bralette_open_shirt",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position on coworking carpet. viewer abdomen, open shorts, thighs, knees, and feet form the lower border. the woman kneels directly below the viewer between his feet. the woman wears a fitted dark bralette and an open light button-down shirt draped from her shoulders. the large penis is centered as a compact vertical cylinder. her mouth seals around the centered large penis, both hands wrapped low at the base. hair crown, eyes, shoulders, bralette edge, open shirt collar, hands, and knees are visible from above. desk legs and chair wheels frame the carpet plane. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "nadir",
|
||||
"clothing_visibility": "woman_bralette_open_button_down",
|
||||
"hand_position": "both_hands_low_base",
|
||||
"workspace_surface": "carpet_desk_chairs"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 114
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_bralette_open_shirt"
|
||||
},
|
||||
"notes": "tests woman-owned clothing with office outfit"
|
||||
},
|
||||
{
|
||||
"id": "axis_low_lounge_table",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position beside a low coworking lounge table. viewer looks down past abdomen, shorts, thighs, knees, and feet. the carpet floor plane dominates the frame. a low table corner and table legs appear as flat top-down workspace shapes near the woman's shoulder. the large penis rises from the lower center as a compact vertical column. the woman kneels directly below the viewer, mouth sealed around the centered large penis, both hands cupping the base. her hair crown, eyes, shoulders, hands, and knees are visible from above. chair wheels and carpet seams continue the overhead office read around the table. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "standing_top_view",
|
||||
"workspace_surface": "low_lounge_table_edge",
|
||||
"hand_position": "both_hands_cupping_base",
|
||||
"body_angle": "kneeling_beside_low_table"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 115
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_low_lounge_table"
|
||||
},
|
||||
"notes": "tests workspace interaction with low table"
|
||||
},
|
||||
{
|
||||
"id": "axis_wide_floor_coworking_rows",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High top-down standing male POV oral position with a wider coworking floor read. viewer abdomen, open shorts, thighs, knees, and bare feet stay in the lower foreground. the woman kneels between his feet on the carpet, smaller under the higher downward camera. rows of desk legs, chair wheels, table corners, and carpet seams extend across the floor around her. the large penis remains a centered compact vertical column from the lower edge to her mouth. her mouth seals around it and one hand wraps the base. hair crown, shoulders, hands, knees, and the floor grid are all visible from above. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "higher_top_down_wide_floor",
|
||||
"workspace_surface": "coworking_rows_floor_grid",
|
||||
"contact_depth": "compact_centered_contact",
|
||||
"hand_position": "one_hand_base"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 116
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_wide_floor_coworking_rows"
|
||||
},
|
||||
"notes": "tests a wider floor-dominant frame"
|
||||
},
|
||||
{
|
||||
"id": "axis_tight_vertical_crop",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Tight nadir standing male POV oral position. viewer abdomen and open shorts fill the bottom edge, thighs and feet appear close at the lower sides. the large penis is a compact centered vertical column occupying the middle. the woman's mouth seals around the centered tip directly below the viewer's torso. her hair crown, forehead, eyelashes, hands, shoulders, and knees appear around the shaft from above. both hands hold the base with fingers stacked. carpet texture fills every background gap, with only nearby desk legs and chair wheels at the side edges. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "tight_nadir_crop",
|
||||
"contact_depth": "mouth_directly_below_tip",
|
||||
"hand_position": "stacked_fingers_base",
|
||||
"workspace_surface": "carpet_background_gaps"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 117
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_tight_vertical_crop"
|
||||
},
|
||||
"notes": "tests tight crop and vertical shaft dominance"
|
||||
},
|
||||
{
|
||||
"id": "axis_viewer_feet_frame_head",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position with the viewer's bare feet framing the woman's head. the camera looks down from the viewer's torso to the carpet. viewer abdomen, shorts, thighs, knees, and feet form a clear lower frame. the woman's head sits centered between the feet, shoulders below the feet line, knees farther into the carpet plane. the large penis is a compact centered vertical column entering her mouth. her mouth seals around it, left hand at the base, right hand on the viewer's shin. desk legs, chair wheels, carpet seams, and floor texture show the same overhead office angle around the body. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "standing_feet_frame_head",
|
||||
"body_angle": "head_between_feet_shoulders_below",
|
||||
"hand_position": "base_and_viewer_shin",
|
||||
"workspace_surface": "overhead_office_angle"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 118
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_viewer_feet_frame_head"
|
||||
},
|
||||
"notes": "tests viewer standing geometry through feet/head relation"
|
||||
},
|
||||
{
|
||||
"id": "axis_phone_like_top_snapshot",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Phone-like standing male POV top snapshot from above the viewer's torso. viewer abdomen, open shorts, thighs, knees, and bare feet anchor the lower part of the frame. the camera points almost straight down to the carpet. the large penis appears as a centered foreshortened vertical column. the woman kneels directly below between the viewer's feet, looking upward with dark almond eyes, mouth sealed around the centered tip. one hand wraps the base and the other hand holds the viewer's thigh. desk legs, chair wheels, carpet seams, laptop tables, and glass partition floor rails appear as top-down coworking details around her. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "phone_like_top_snapshot",
|
||||
"expression_eye_detail": "upward_eyes",
|
||||
"hand_position": "base_and_viewer_thigh",
|
||||
"workspace_surface": "laptop_tables_floor_rails"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 119
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_phone_like_top_snapshot"
|
||||
},
|
||||
"notes": "tests phone-like top snapshot wording"
|
||||
},
|
||||
{
|
||||
"id": "axis_floor_plane_priority",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Floor-plane-priority standing male POV oral position. viewer stands above the kneeling woman and looks down from his abdomen. open shorts, thighs, feet, and lower abdomen frame the bottom. the carpet plane is the main background surface, filled with woven texture, floor seams, desk-leg feet, and caster wheels. the large penis rises from the lower center as a foreshortened vertical cylinder. the woman kneels in the middle of the carpet plane, mouth sealed around the centered large penis. both hands wrap around the base, shoulders and knees visible below her hair crown. her dark eyes angle up toward the viewer. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"cue_axes": {
|
||||
"camera_height": "floor_plane_priority",
|
||||
"workspace_surface": "woven_carpet_seams_wheels",
|
||||
"hand_position": "both_hands_wrap_base",
|
||||
"expression_eye_detail": "eyes_angle_up"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": 238365845574312,
|
||||
"atlas_cue_seed": 120
|
||||
},
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "axis_floor_plane_priority"
|
||||
},
|
||||
"notes": "tests floor-priority wording as a strong axis cue"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"probes": [
|
||||
{
|
||||
"id": "baseline_top_down_vertical_shaft",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 6,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_8c3ae3ebaff74bee91cdcdb600163c79.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_floor_fills_frame",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 7,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_a89acdeb80f641c59fef7352b6c20661.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_standing_feet_close",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 8,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_57ffe9aa6c7d4dcda6d49937b1dcdc5c.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_desk_leg_grid",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 9,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_e85bd81bef114bf1b741dc816254881a.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_carpet_seam_centerline",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 10,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_c13ee92586ed4310bf1bd23626eee5f0.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_chair_wheel_floor",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 11,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_5265517ff9544c149277a8711461da17.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_under_table_edge",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 12,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_396bd047524b4c92b46d5860fec682da.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_glass_partition_floor",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 13,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_69a268a1cf3142f39d99d159853e92df.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_mouth_directly_below_torso",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 14,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_75cd8e71ddad45f4a4b2aa9e00ea6127.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_knees_visible_below_head",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 15,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_22bfc679e02b4b9b9d3ee21a9a302d03.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_compact_anatomy",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 16,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_cef16a8cebd649c4a53191f9f74f21e7.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_eye_contact_vertical",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 17,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_acfdcfda262849eea33dd5b31985751c.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_soft_expression",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 18,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_3f455403ee98473e8d30ef34d93ac9d1.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_clothed_top_visible",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 19,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_13516b4bed1a4a69a8542b446822836e.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_bralette_open_shirt",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 20,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_e4f7ec25182b4acfa4ecce291a0a5097.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_low_lounge_table",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 21,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_eda27adbb98648b492444867f764d4c9.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_wide_floor_coworking_rows",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 22,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_696bb9f3163049f5b696316db97b46f2.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_tight_vertical_crop",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 23,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_9840c47c74f7444ab5a7b8afd24189f4.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_viewer_feet_frame_head",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 24,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_11315ff26c78428fb5e630d9c4fd7ac5.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_phone_like_top_snapshot",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 25,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_da68aff4f3234e51a4dca2196dc8d9ae.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "axis_floor_plane_priority",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 26,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_867f5eea66354fb4beb5df586e56bfbc.png",
|
||||
"returned_seed": 238365845574312
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
# Blowjob Top-Down Vertical Shaft Floor-Plan Salvage
|
||||
|
||||
Date: 2026-07-01
|
||||
|
||||
Pose target: `pov_blowjob_top_down_vertical_shaft`
|
||||
|
||||
Reference atlas examples:
|
||||
|
||||
- `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/blowjob_top_view/27_blowjob_top_view.png`
|
||||
- `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/blowjob_top_view/2_blowjob_top_view.png`
|
||||
|
||||
Key manual evidence:
|
||||
|
||||
- `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00062_.png`
|
||||
- `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00079_.png`
|
||||
|
||||
MCP batches:
|
||||
|
||||
- `ab_batches/blowjob_top_down_vertical_shaft_overhead_floorplan_seed_238365845574312_batch.json`
|
||||
- `ab_batches/blowjob_top_down_vertical_shaft_overhead_floorplan_seed_238365845574312_results.json`
|
||||
- `ab_batches/blowjob_top_down_vertical_shaft_minimal_floor_tail_seed_238365845574312_batch.json`
|
||||
- `ab_batches/blowjob_top_down_vertical_shaft_minimal_floor_tail_seed_238365845574312_results.json`
|
||||
|
||||
Sampler seed: `238365845574312`
|
||||
|
||||
## Atlas Comparison
|
||||
|
||||
The atlas frames do not mainly solve the pose by saying “top view” many times.
|
||||
They solve it by making the environment match the camera axis:
|
||||
|
||||
- the background is a floor or ground plane, not a deep room;
|
||||
- environmental evidence is flat to the camera, such as floor boards, turf, net,
|
||||
or cropped edge objects;
|
||||
- the partner is below the camera, and her head/crown/shoulders stack downward
|
||||
in the image;
|
||||
- foreground viewer body cues sit at the lower edge, but they do not require a
|
||||
broad rendered room behind the partner.
|
||||
|
||||
The original coworking scene tail fought that geometry because `tall windows`,
|
||||
`repeated desk rows`, and `soft shared-office depth` invite a forward-looking
|
||||
room render. More vertical synonyms did not fully fix that conflict.
|
||||
|
||||
## Working Prompt Principle
|
||||
|
||||
For this pose, translate the coworking lounge into top-down floor evidence:
|
||||
|
||||
```text
|
||||
Set in a coworking lounge seen as a top-down floor plan: carpet texture,
|
||||
carpet tile seams
|
||||
```
|
||||
|
||||
This is not a generic “remove background” rule. It is a pose-specific scene
|
||||
translation. A different atlas family may need more environment detail if the
|
||||
reference angle actually shows a room, desk surface, sofa, wall, bed, or other
|
||||
support.
|
||||
|
||||
## Strong Current Candidates
|
||||
|
||||
- `manual_00079_minimal_tail`
|
||||
- image: `/media/unraid/comfyui/output/agent_bridge/img_c44d156d05614e00858dbe659be7ebc0.png`
|
||||
- prompt keeps the manual `img_00079` minimal scene tail.
|
||||
- Strong overhead read, carpet-dominant, still plausible coworking floor.
|
||||
- `minimal_tail_floor_plane_background`
|
||||
- image: `/media/unraid/comfyui/output/agent_bridge/img_726db4d0591647c2a2ed39a93f37ccb9.png`
|
||||
- Strong overhead and clean floor-plane background.
|
||||
- Slightly more generic floor material than the coworking carpet-tail version.
|
||||
- `minimal_tail_tight_crop`
|
||||
- image: `/media/unraid/comfyui/output/agent_bridge/img_4764ba4774974d9789df7fdaa12b1f65.png`
|
||||
- Strong contact priority and verticality.
|
||||
- More cropped; needs user verification against atlas preference.
|
||||
|
||||
## Weaker Add-Backs
|
||||
|
||||
Adding explicit office objects too early can reintroduce room-depth pressure.
|
||||
Single or double anchors such as `one cropped chair caster`, `one cropped desk
|
||||
foot`, or both may be useful for scene identity, but they should be added only
|
||||
after the vertical floor-plane read is preserved across seeds.
|
||||
|
||||
## Current Decision
|
||||
|
||||
This pose is no longer a text-weak case. The failure was mostly a scene/camera
|
||||
conflict:
|
||||
|
||||
- bad: overhead pose wording plus deep coworking room-depth tail;
|
||||
- better: overhead pose wording plus floor-plan coworking tail;
|
||||
- best current hypothesis: overhead pose wording plus minimal carpet/tile-seam
|
||||
coworking floor tail.
|
||||
|
||||
Next gate: user visual verification of the strongest candidate, then repeat
|
||||
the selected minimal-tail candidate on at least one second sampler seed before
|
||||
sidecar/catalog cue promotion.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Blowjob Top-Down Vertical Shaft Image-To-Prompt Calibration
|
||||
|
||||
Date: 2026-07-01
|
||||
|
||||
Pose target: `pov_blowjob_top_down_vertical_shaft`
|
||||
|
||||
Primary atlas reference:
|
||||
|
||||
- `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/blowjob_top_view/22_blowjob_top_view.png`
|
||||
|
||||
Manual calibration evidence:
|
||||
|
||||
- `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00100_.png`
|
||||
- `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00101_.png`
|
||||
|
||||
MCP image-to-prompt batch:
|
||||
|
||||
- `ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_seed_238365845574312_batch.json`
|
||||
- `ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_seed_238365845574312_results.json`
|
||||
|
||||
Sampler seed: `238365845574312`
|
||||
|
||||
## What Improved
|
||||
|
||||
The strongest verticality came from describing the camera and scene as a single
|
||||
top-down plane, not from repeating more top-view synonyms. The working manual
|
||||
prompt removed the deep coworking-room tail and used a flat floor/support read:
|
||||
|
||||
```text
|
||||
Background reads as a flat pale floor and cropped white lounge chair surface,
|
||||
with very little room depth.
|
||||
```
|
||||
|
||||
This confirms the earlier conflict analysis: for this atlas family, deep room
|
||||
phrases such as repeated desk rows, tall windows, and soft background depth
|
||||
fight the vertical camera axis.
|
||||
|
||||
## Remaining Miss
|
||||
|
||||
The calibration images have the best verticality so far, but compared with atlas
|
||||
22 the viewer foreground is still too dominant. The atlas reference gives more
|
||||
visual weight to the woman's face, hair crown, shoulders, upper chest, and hand
|
||||
stack. The next prompt rule should push that positive hierarchy, rather than
|
||||
adding room detail or relying on negative-style body suppression.
|
||||
|
||||
## Prompt Rule
|
||||
|
||||
Use a positive visibility order:
|
||||
|
||||
```text
|
||||
Straight-down male POV oral close-up. The camera looks almost vertically down
|
||||
from the man's upper abdomen. The woman's face, eyelids, hair crown, shoulders,
|
||||
upper chest, and one hand stack directly below the camera. Centered mouth
|
||||
contact aligns with the vertical shaft. One hand wraps the base near the man's
|
||||
lower abdomen. The man's lower abdomen and small thigh edges anchor only the
|
||||
bottom foreground. Tucked knees remain small side shapes on the floor. The
|
||||
background reads as a flat pale floor and one cropped white lounge chair
|
||||
surface, with shallow top-down room depth.
|
||||
```
|
||||
|
||||
For clothed atlas variants, use subject-owned clothing as a geometry anchor:
|
||||
|
||||
```text
|
||||
The woman wears a fitted white ribbed tank top; the tank-top neckline and
|
||||
shoulders remain visible from above.
|
||||
```
|
||||
|
||||
Avoid carrying the manual scoring phrase `hips and ass stay visually secondary,
|
||||
mostly hidden` into final positive conditioning. Keep that as a human rejection
|
||||
criterion, and express the render prompt through the visible upper-body stack.
|
||||
|
||||
## Current Decision
|
||||
|
||||
This pose is prompt-responsive. The route should not be promoted yet from these
|
||||
two manual calibrations alone, but the next batch should keep the successful
|
||||
floor/support-plane camera tail fixed and compare upper-body-stack variants,
|
||||
including the fitted-tank-top anchor that previously matched atlas 22 more
|
||||
closely than the nude minimal-floor candidates.
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_out": "sxcp_eval_out",
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"subject_id": "atlas_refine_same_woman_001",
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft",
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"selection": {
|
||||
"purpose": "image-to-prompt inversion from clothed generated atlas-like result and atlas_22 body geometry",
|
||||
"sampler_seed_role": "fixed sampler seed for comparison against minimal_tail_woman_tank_top"
|
||||
},
|
||||
"probes": [
|
||||
{
|
||||
"id": "clothed_minimal_tail_control",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["control", "owned_clothing", "minimal_scene_tail"],
|
||||
"evidence": {
|
||||
"prior_image": "/media/unraid/comfyui/output/agent_bridge/img_556bc9272211426cb6e0139c4ec0dd0a.png",
|
||||
"atlas_reference": "/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/blowjob_top_view/22_blowjob_top_view.png"
|
||||
},
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. tank top shoulders and neckline remain visible from above. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams"
|
||||
},
|
||||
{
|
||||
"id": "upper_body_stack_tank_top",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["image_to_prompt", "upper_body_stack", "owned_clothing"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen onto the carpet floor. her hair crown, forehead, face, white tank-top neckline, shoulders, hands, and tucked knees stack vertically below the camera. her mouth seals around the centered foreshortened large penis. both hands wrap the base directly under her lips. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams"
|
||||
},
|
||||
{
|
||||
"id": "atlas_22_torso_neckline",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["image_to_prompt", "atlas_22_body_geometry", "torso_neckline"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen. the woman's face, tank-top neckline, shoulders, and upper torso are the main partner shapes below the camera. her knees tuck under her body on the carpet behind the shoulder line. her mouth seals around the centered foreshortened large penis. both hands wrap the base. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams"
|
||||
},
|
||||
{
|
||||
"id": "upright_kneeling_torso",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["image_to_prompt", "upright_kneeling", "body_proportion"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman kneels upright below the viewer with shoulders and tank-top chest centered under her head. her knees are tucked on the carpet at the sides of the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base under her lips. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams"
|
||||
},
|
||||
{
|
||||
"id": "shoulders_primary_knees_small",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["image_to_prompt", "shoulder_priority", "knee_scale"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen. her head, face, shoulders, and white tank-top upper torso dominate the partner silhouette. her tucked knees remain smaller side shapes on the carpet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and sit between the viewer's feet. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams"
|
||||
},
|
||||
{
|
||||
"id": "tank_top_eye_contact",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["image_to_prompt", "eye_contact", "owned_clothing"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen. her face tilts upward, dark almond eyes looking into the camera, with the white tank-top neckline and shoulders visible below her face. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her tucked knees remain low on the carpet. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams"
|
||||
},
|
||||
{
|
||||
"id": "tank_top_one_caster_edge",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["image_to_prompt", "owned_clothing", "minimal_workspace_anchor"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen onto the carpet floor. her face, white tank-top neckline, shoulders, hands, and tucked knees stack below the camera. her mouth seals around the centered foreshortened large penis. both hands wrap the base. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, one cropped chair caster at the far edge"
|
||||
},
|
||||
{
|
||||
"id": "tight_tank_top_stack",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["image_to_prompt", "tight_crop", "upper_body_stack"],
|
||||
"text": "Tight straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen. her face, shoulders, white tank-top upper torso, hands, and tucked knees fill the center of the carpet floor. her mouth seals around the centered foreshortened large penis. both hands wrap the base directly under her lips. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams"
|
||||
}
|
||||
]
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"probes": [
|
||||
{
|
||||
"id": "clothed_minimal_tail_control",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 78,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_d5264d677f954800aaf5d71b11249e32.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "upper_body_stack_tank_top",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 79,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_907d592ed7b44e6dacf6a3d22a7e48f1.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "atlas_22_torso_neckline",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 80,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_9dfec0d52f8d4e468bfe639e94e0f382.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "upright_kneeling_torso",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 81,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_5ebc5a243b6247bf990769bb902d7563.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "shoulders_primary_knees_small",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 82,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_b39a8cfe8f9c4819b12d472f931a0e55.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "tank_top_eye_contact",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 83,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_ec875770f68144c3a495b186da589848.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "tank_top_one_caster_edge",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 84,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_aa78fceb96c14f17a585c0adcc9c0ddf.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "tight_tank_top_stack",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 85,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_32c138249203481290985c29c48960fe.png",
|
||||
"returned_seed": 238365845574312
|
||||
}
|
||||
]
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_out": "sxcp_eval_out",
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"subject_id": "atlas_refine_same_woman_001",
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft",
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"selection": {
|
||||
"purpose": "minimal coworking floor-tail probes from manual img_00079 conflict-analysis evidence",
|
||||
"sampler_seed_role": "fixed sampler seed for direct comparison against sparse manual evidence"
|
||||
},
|
||||
"probes": [
|
||||
{
|
||||
"id": "manual_00079_minimal_tail",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["manual_evidence", "minimal_scene_tail", "carpet_only"],
|
||||
"evidence": {
|
||||
"manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00079_.png",
|
||||
"manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00079_.txt"
|
||||
},
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams"
|
||||
},
|
||||
{
|
||||
"id": "minimal_tail_with_one_caster",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["minimal_scene_tail", "single_office_anchor", "sparse_floor"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, one cropped chair caster at the far edge"
|
||||
},
|
||||
{
|
||||
"id": "minimal_tail_with_edge_desk_foot",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["minimal_scene_tail", "single_office_anchor", "desk_foot_edge"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, one cropped desk foot at the frame edge"
|
||||
},
|
||||
{
|
||||
"id": "minimal_tail_with_caster_and_desk_foot",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["minimal_scene_tail", "two_office_anchors", "sparse_floor"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, one cropped chair caster, one cropped desk foot"
|
||||
},
|
||||
{
|
||||
"id": "carpet_only_no_lounge_word",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["scene_tail_removed", "carpet_only", "conflict_reduction"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Carpet texture and carpet tile seams fill the background floor plane."
|
||||
},
|
||||
{
|
||||
"id": "minimal_tail_floor_plane_background",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["floor_as_background", "minimal_scene_tail", "top_view"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen. the carpet floor plane is the background. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen from above: carpet texture and carpet tile seams."
|
||||
},
|
||||
{
|
||||
"id": "minimal_tail_tight_crop",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["tight_crop", "minimal_scene_tail", "contact_priority"],
|
||||
"text": "Tight straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams"
|
||||
},
|
||||
{
|
||||
"id": "minimal_tail_woman_tank_top",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["owned_clothing", "minimal_scene_tail", "pose_preservation"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. tank top shoulders and neckline remain visible from above. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams"
|
||||
}
|
||||
]
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"probes": [
|
||||
{
|
||||
"id": "manual_00079_minimal_tail",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 70,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_c44d156d05614e00858dbe659be7ebc0.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "minimal_tail_with_one_caster",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 71,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_a75439104c734857ae2f1cc39cbd54a5.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "minimal_tail_with_edge_desk_foot",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 72,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_9798ccfcef9e47d3bce59465bc236d15.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "minimal_tail_with_caster_and_desk_foot",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 73,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_c98adad644254d81b69e7d52bf076003.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "carpet_only_no_lounge_word",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 74,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_fa66fc003bbb499887572e7becc68bbc.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "minimal_tail_floor_plane_background",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 75,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_726db4d0591647c2a2ed39a93f37ccb9.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "minimal_tail_tight_crop",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 76,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_4764ba4774974d9789df7fdaa12b1f65.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "minimal_tail_woman_tank_top",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 77,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_556bc9272211426cb6e0139c4ec0dd0a.png",
|
||||
"returned_seed": 238365845574312
|
||||
}
|
||||
]
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_out": "sxcp_eval_out",
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"subject_id": "atlas_refine_same_woman_001",
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft",
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"selection": {
|
||||
"purpose": "top-view oral salvage with overhead camera words and floor-plan coworking scene wording",
|
||||
"sampler_seed_role": "fixed sampler seed for direct comparison against prior MCP and manual overhead probes"
|
||||
},
|
||||
"probes": [
|
||||
{
|
||||
"id": "baseline_ref_folder",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["baseline", "folder_reference"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir-angle standing male POV top-view oral position. viewer looks almost straight down from his torso toward the floor. nearby carpet floor plane dominates the image. viewer abdomen, shorts, thighs, and feet frame the lower foreground. large penis is a short centered vertical column. the woman kneels directly below the viewer between his feet. her mouth seals around the centered large penis. one hand wraps the base. hair crown, forehead, shoulders, hands, and knees are visible from above. desk legs, chair wheels, carpet texture, and floor seams act as top-down office anchors. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "manual_00062_original_scene_control",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["manual_evidence", "overhead_prefix", "original_scene_tail"],
|
||||
"evidence": {
|
||||
"manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00062_.png",
|
||||
"manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00062_.txt"
|
||||
},
|
||||
"text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "overhead_floorplan_scene_tail",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["overhead_prefix", "straight_down", "floorplan_scene_tail"],
|
||||
"text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman."
|
||||
},
|
||||
{
|
||||
"id": "perfectly_vertical_floorplan",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["perfectly_vertical", "floorplan_scene_tail", "strong_camera_axis"],
|
||||
"text": "Perfectly vertical overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman."
|
||||
},
|
||||
{
|
||||
"id": "straight_down_floorplan",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["straight_down_prefix", "floorplan_scene_tail", "camera_axis"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman."
|
||||
},
|
||||
{
|
||||
"id": "floor_plan_view",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["floor_plan_view", "scene_reconciliation", "workspace_floor"],
|
||||
"text": "Top-down floor-plan view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV oral position. viewer looks straight down from his abdomen onto the carpet floor. the woman kneels below the viewer in the middle of the floor-plan composition. her mouth seals around the centered foreshortened large penis. both hands wrap the base. her hair crown, forehead, shoulders, hands, and knees read from above. Coworking lounge objects read as floor-plan anchors: carpet seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots around her."
|
||||
},
|
||||
{
|
||||
"id": "perpendicular_camera_axis",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["camera_axis", "technical_verticality", "floorplan_scene_tail"],
|
||||
"text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. the camera axis is perpendicular to the carpet floor and points straight down from the viewer's abdomen. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman."
|
||||
},
|
||||
{
|
||||
"id": "ceiling_to_floor_alignment",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["plumb_alignment", "floorplan_scene_tail", "overhead_prefix"],
|
||||
"text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Plumb vertical standing male POV oral position. viewer looks straight down from his abdomen to the carpet floor. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman."
|
||||
},
|
||||
{
|
||||
"id": "floor_fills_frame",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["floor_ratio", "floorplan_scene_tail", "workspace_floor"],
|
||||
"text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen. carpet floor fills the frame around the kneeling woman. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Coworking lounge floor details surround her: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots."
|
||||
},
|
||||
{
|
||||
"id": "woman_tank_top_floorplan",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["owned_clothing", "floorplan_scene_tail", "pose_preservation"],
|
||||
"text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. tank top shoulders and neckline remain visible from above. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman."
|
||||
},
|
||||
{
|
||||
"id": "eye_contact_floorplan",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["eye_control", "floorplan_scene_tail", "pose_preservation"],
|
||||
"text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her face tilts upward and her dark almond eyes look up into the camera. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman."
|
||||
},
|
||||
{
|
||||
"id": "compact_floorplan_no_camera_tail",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["short_prompt", "floorplan_scene_tail", "noise_reduction"],
|
||||
"text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen onto the carpet floor. the woman kneels in the center of the floor below him. her mouth seals around the centered foreshortened large penis. both hands wrap the base. her hair crown, forehead, shoulders, hands, and knees read from above. Coworking lounge floor-plan anchors surround her: carpet seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots."
|
||||
}
|
||||
]
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"probes": [
|
||||
{
|
||||
"id": "baseline_ref_folder",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 58,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_7c307b0667b04c9190b657ec0c59eace.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "manual_00062_original_scene_control",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 59,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_21686bf41ca74e16a689bcb58b818213.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "overhead_floorplan_scene_tail",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 60,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_498ada9c2dab4adfb03cd1e2e5655a39.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "perfectly_vertical_floorplan",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 61,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_a677480fbf3241d1b912630e1c659dc4.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "straight_down_floorplan",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 62,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_02410cd85ef14ceaa160cca11116f5cb.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "floor_plan_view",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 63,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_6383798de55c40e68373061e0d2d4890.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "perpendicular_camera_axis",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 64,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_2b649968429e4a7d97ea2e08ecc20484.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "ceiling_to_floor_alignment",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 65,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_cc7c76a2226f4949897d20e80108dc55.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "floor_fills_frame",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 66,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_ce49b6650d2845f6a297703af02fdcf5.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "woman_tank_top_floorplan",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 67,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_cdb2eb93082d4369990b52c33d0254f6.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "eye_contact_floorplan",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 68,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_89b27cdbb89a4d999c52f6eba8a23cd2.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "compact_floorplan_no_camera_tail",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 69,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_c86cc56829b14a7eb30d4fd30f95c0fd.png",
|
||||
"returned_seed": 238365845574312
|
||||
}
|
||||
]
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_out": "sxcp_eval_out",
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"subject_id": "atlas_refine_same_woman_001",
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft",
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"selection": {
|
||||
"purpose": "salvage top-view oral using user manual overhead prompt evidence from sxcp_accumulator/bwave_2",
|
||||
"sampler_seed_role": "fixed sampler seed for direct comparison with previous MCP probes"
|
||||
},
|
||||
"probes": [
|
||||
{
|
||||
"id": "baseline_ref_folder",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["baseline", "folder_reference"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir-angle standing male POV top-view oral position. viewer looks almost straight down from his torso toward the floor. nearby carpet floor plane dominates the image. viewer abdomen, shorts, thighs, and feet frame the lower foreground. large penis is a short centered vertical column. the woman kneels directly below the viewer between his feet. her mouth seals around the centered large penis. one hand wraps the base. hair crown, forehead, shoulders, hands, and knees are visible from above. desk legs, chair wheels, carpet texture, and floor seams act as top-down office anchors. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "prior_low_head_high_floor_ratio",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["previous_probe_control", "floor_ratio", "no_overhead_prefix"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs, and feet sit at the bottom edge. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "manual_00057_vertical_overhead",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["manual_evidence", "vertical_overhead_prefix", "full_lower_body_sentence"],
|
||||
"evidence": {
|
||||
"manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00057_.png",
|
||||
"manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00057_.txt"
|
||||
},
|
||||
"text": "Vertical overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs, and feet sit at the bottom edge. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "manual_00059_top_vertical_truncated",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["manual_evidence", "top_vertical_overhead_prefix", "truncated_lower_body_sentence"],
|
||||
"evidence": {
|
||||
"manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00059_.png",
|
||||
"manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00059_.txt"
|
||||
},
|
||||
"text": "Top Vertical overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "manual_00060_completely_vertical",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["manual_evidence", "completely_vertical_overhead_prefix", "truncated_lower_body_sentence"],
|
||||
"evidence": {
|
||||
"manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00060_.png",
|
||||
"manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00060_.txt"
|
||||
},
|
||||
"text": "Completely Vertical overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "manual_00061_vertical_ceiling",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["manual_evidence", "vertical_ceiling_prefix", "truncated_lower_body_sentence"],
|
||||
"evidence": {
|
||||
"manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00061_.png",
|
||||
"manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00061_.txt"
|
||||
},
|
||||
"text": "Vertical ceiling view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "manual_00062_overhead_straight_down",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["manual_evidence", "overhead_prefix", "straight_down", "minimal_lower_body_sentence"],
|
||||
"evidence": {
|
||||
"manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00062_.png",
|
||||
"manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00062_.txt"
|
||||
},
|
||||
"text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "manual_00063_overhead_camera_vertical",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["manual_evidence", "overhead_prefix", "straight_down", "camera_vertical_phrase"],
|
||||
"evidence": {
|
||||
"manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00063_.png",
|
||||
"manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00063_.txt"
|
||||
},
|
||||
"text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is overhead vertical male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "overhead_straight_down_woman_tank_top",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["owned_clothing", "overhead_prefix", "pose_preservation"],
|
||||
"text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. tank top shoulders and neckline remain visible from above. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "overhead_straight_down_eye_contact",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["facial_expression", "eye_control", "overhead_prefix", "pose_preservation"],
|
||||
"text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her face tilts upward and her dark almond eyes look up into the camera. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
# Blowjob Top-Down Vertical Shaft Refine Pass
|
||||
|
||||
Date: 2026-07-01
|
||||
|
||||
Pose target: `pov_blowjob_top_down_vertical_shaft`
|
||||
|
||||
Reference folder baseline:
|
||||
`/media/unraid/comfyui/output/CodexMCP-Atlas-Refine/pov_blowjob_top_down_vertical_shaft_00001_.png`
|
||||
|
||||
Prior batch:
|
||||
`ab_batches/blowjob_top_down_vertical_shaft_axis_batch.json`
|
||||
|
||||
Second-pass batches:
|
||||
|
||||
- `ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574312_batch.json`
|
||||
- `ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574313_batch.json`
|
||||
|
||||
Second-pass results:
|
||||
|
||||
- `ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574312_results.json`
|
||||
- `ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574313_results.json`
|
||||
|
||||
Sampler seeds:
|
||||
|
||||
- `238365845574312`
|
||||
- `238365845574313`
|
||||
|
||||
Total text-only attempts counted for this pose: 51 prompt/seed outcomes
|
||||
across the first and second MCP batches.
|
||||
|
||||
## Result
|
||||
|
||||
The second pass is stable but does not solve the atlas top-view geometry. Most
|
||||
variants converge to a front-facing kneeling or seated oral composition: contact
|
||||
is often preserved, subject identity stays strong, and the coworking floor grid
|
||||
is coherent, but the camera still reads forward/downward rather than true nadir.
|
||||
|
||||
This means the exact atlas family shown by `blowjob_top_view` references is a
|
||||
weak text-only case for this model under subject-first prompting. Treat it as a
|
||||
candidate for stronger image/control guidance if a flatter atlas match is
|
||||
required.
|
||||
|
||||
## Best Repeatable Partial
|
||||
|
||||
The best repeatable partial remains the first-pass direction:
|
||||
|
||||
- `axis_mouth_directly_below_torso`
|
||||
- `axis_floor_plane_priority`
|
||||
|
||||
The refined `floor_plane_mouth_under_torso` prompt preserved contact on both
|
||||
sampler seeds and produced stable office-floor framing:
|
||||
|
||||
- seed `238365845574312`: `/media/unraid/comfyui/output/agent_bridge/img_b6525cf541d44c34828c7fe19068425b.png`
|
||||
- seed `238365845574313`: `/media/unraid/comfyui/output/agent_bridge/img_fe09b3d132004f26ab848f3edb21d8a0.png`
|
||||
|
||||
It is better than a generic seated oral prompt because it anchors the partner
|
||||
below the viewer, keeps the mouth/contact centered, and uses floor/caster/desk
|
||||
anchors. It is not a proven exact top-view atlas reproduction.
|
||||
|
||||
## Stable But Insufficient
|
||||
|
||||
These cues mostly improved consistency rather than verticality:
|
||||
|
||||
- `caster_wheel_floor_grid`
|
||||
- `desk_leg_rows_floor_depth`
|
||||
- `viewer_feet_gate_head_center`
|
||||
- `straight_down_carpet_tiles`
|
||||
- `mouth_below_navel_eye_contact`
|
||||
- `kneeling_between_shoes_floor_map`
|
||||
- `short_column_contact_scale`
|
||||
- `desk_edges_side_floor_anchor`
|
||||
- `head_crown_forehead_vertical_read`
|
||||
- `office_lane_floor_perspective`
|
||||
- `low_head_high_floor_ratio`
|
||||
|
||||
They tend to preserve the coworking deck and subject identity, but they do not
|
||||
force a flat top-view body plane. Several reduce the action to a conventional
|
||||
front-facing kneeling oral pose.
|
||||
|
||||
## Weak Or Rejected Axes
|
||||
|
||||
- `phone_snapshot_abdomen_down` often broke mouth contact and produced a staged
|
||||
kneeling pose.
|
||||
- `woman_tank_top_owned_visibility` confirmed owned clothing works, but it did
|
||||
not improve the top-view geometry.
|
||||
- More repetitions of `nadir`, `straight-down`, `floor plane`, `viewer feet`,
|
||||
and `desk-leg grid` are unlikely to solve the exact atlas pose alone.
|
||||
|
||||
## Prompt Rules From This Pass
|
||||
|
||||
- Keep as a provisional prompt-guide partial: `mouth directly below the viewer's
|
||||
torso` plus `floor-plane-priority` office anchors.
|
||||
- Keep office-floor anchors concrete: `chair caster wheels`, `desk-leg feet`,
|
||||
`table bases`, `carpet texture`, `floor seams`.
|
||||
- Owned clothing remains valid only when phrased as `The woman wears ...`.
|
||||
- Do not promote exact top-view success from this pass.
|
||||
- For catalog seed variants, this pose needs either a partial seed family
|
||||
labeled as forward/downward POV oral, or stronger control/image guidance for
|
||||
the flatter atlas top-view family.
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_out": "sxcp_eval_out",
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"subject_id": "atlas_refine_same_woman_001",
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft",
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"selection": {
|
||||
"purpose": "second-pass top-view axis refinement using folder baseline and first MCP batch evidence",
|
||||
"sampler_seed_role": "fixed sampler seed for repeatable matrix comparison"
|
||||
},
|
||||
"probes": [
|
||||
{
|
||||
"id": "baseline_ref_folder",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["baseline", "folder_reference"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir-angle standing male POV top-view oral position. viewer looks almost straight down from his torso toward the floor. nearby carpet floor plane dominates the image. viewer abdomen, shorts, thighs, and feet frame the lower foreground. large penis is a short centered vertical column. the woman kneels directly below the viewer between his feet. her mouth seals around the centered large penis. one hand wraps the base. hair crown, forehead, shoulders, hands, and knees are visible from above. desk legs, chair wheels, carpet texture, and floor seams act as top-down office anchors. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "floor_plane_mouth_under_torso",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["floor_plane_priority", "mouth_under_torso", "contact_preservation"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Floor-plane-priority standing male POV oral position. viewer stands above the kneeling woman and looks down from his abdomen. open shorts, thighs, feet, and lower abdomen frame the bottom. the carpet plane is the main background surface, filled with woven texture, floor seams, desk-leg feet, and caster wheels. the large penis rises from the lower center as a foreshortened vertical cylinder. the woman kneels in the middle of the carpet plane, mouth sealed around the centered large penis. both hands wrap around the base, shoulders and knees visible below her hair crown. her dark eyes angle up toward the viewer. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "caster_wheel_floor_grid",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["caster_wheels", "top_down_grid", "office_floor_anchor"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position on office carpet. the camera looks down past the viewer's abdomen, open shorts, thighs, and feet. chair caster wheels, desk-leg feet, table bases, and carpet seams form a visible grid across the floor. the woman kneels centered inside that floor grid between the viewer's shoes. her hair crown, forehead, shoulders, hands, and knees are seen from above. her mouth seals around the foreshortened centered large penis rising from the lower frame. both hands stack at the base under her lips. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "desk_leg_rows_floor_depth",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["desk_leg_rows", "vertical_depth", "floor_background"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position between coworking desk rows. viewer looks steeply downward from his torso. open shorts, thighs, feet, and lower abdomen hold the lower edge. repeated desk legs and chair wheels recede across the carpet behind the kneeling woman. the floor plane fills most of the image. the woman kneels directly below the viewer, centered between his feet. her mouth covers the short foreshortened large penis at the exact center. both hands hold the base, elbows tucked close, shoulders visible below her hair crown. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "viewer_feet_gate_head_center",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["viewer_feet_frame", "head_centering", "standing_height"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep standing male POV oral position. viewer's bare feet and open shorts create a gate around the bottom of the frame. the camera looks down between his thighs onto the carpet. the woman's head is centered directly below the viewer's pelvis between his feet. her hair crown and forehead face the camera from above while her dark eyes look upward. a short vertical large penis rises from the lower center into her sealed mouth. both hands wrap the base under her lips. floor seams, chair wheels, and desk legs surround her knees on the carpet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "phone_snapshot_abdomen_down",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["phone_like_snapshot", "abdomen_down_angle", "floor_reference"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Phone-like standing male POV oral snapshot from above. the viewer looks down from chest and abdomen height. lower abdomen, open shorts, thighs, and feet occupy the near bottom edge. the carpet below is clear and detailed, with chair wheels, desk-leg feet, and table bases placed around the woman. she kneels directly underneath the viewer between his feet. her mouth seals around the centered short large penis. both hands clasp the base and her shoulders sit below her hair crown. her dark almond eyes glance upward into the camera. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "straight_down_carpet_tiles",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["straight_down_axis", "carpet_tiles", "top_view"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nearly straight-down standing male POV oral position. the viewer's abdomen, shorts, thighs, and feet frame a steep view onto carpet tiles. the carpet tile seams run beneath the kneeling woman and make the camera angle read vertical. she kneels below the viewer with knees spread on the carpet. the short centered large penis points upward from the lower frame into her sealed mouth. both hands hold the base at the viewer's pelvis line. hair crown, forehead, shoulders, hands, and knees are all visible from above. desk legs and chair wheels sit flat on the floor around her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "mouth_below_navel_eye_contact",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["mouth_below_navel", "eye_contact", "contact_preservation"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position with eye contact. viewer looks down past his navel, open shorts, thighs, and feet. the woman's mouth sits directly below the viewer's navel line on the carpet plane. her lips seal around the centered foreshortened large penis and both hands clasp the base. her face tilts upward, dark almond eyes looking into the camera. the hair crown, forehead, shoulders, hands, and knees remain readable from above. carpet texture, desk legs, chair wheels, and table bases flatten into the background floor grid. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "kneeling_between_shoes_floor_map",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["kneeling_between_feet", "floor_map", "standing_reference"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position. the viewer's feet sit near the lower left and lower right edges like fixed markers on the carpet. his open shorts, thighs, and lower abdomen occupy the bottom edge. the woman kneels between those feet, directly under the viewer's pelvis. her head, shoulders, hands, and knees fit inside the visible floor map. the short centered large penis rises from the lower center into her sealed mouth. both hands wrap the base under her lips. office desk legs, caster wheels, carpet seams, and table bases show the flat floor orientation. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "short_column_contact_scale",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["contact_scale", "foreshortened_column", "anatomy_length_control"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep standing male POV oral position with foreshortened contact scale. viewer looks down from his abdomen to the carpet. lower abdomen, open shorts, thighs, and feet frame the bottom foreground. the large penis reads as a short centered vertical column because the camera is directly above it. the woman kneels below the viewer and seals her mouth around the centered tip. both hands clasp low at the base near the viewer's pelvis. her hair crown, forehead, eyes, shoulders, and knees are visible from the top angle. floor seams, desk legs, caster wheels, and chair bases stay flat around her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "woman_tank_top_owned_visibility",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["owned_clothing", "visible_top", "pose_preservation"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. Standing male POV top-view oral position. viewer looks down from his abdomen with open shorts, thighs, and feet framing the lower edge. the woman kneels directly below the viewer on the carpet between his feet. her tank top shoulders and neckline are visible below her hair crown from above. her mouth seals around the centered short large penis. both hands wrap the base under her lips. carpet texture, floor seams, desk legs, caster wheels, and table bases surround her knees and anchor the top-down office floor. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "desk_edges_side_floor_anchor",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["desk_edges", "side_background", "floor_angle"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position beside coworking desks. viewer looks down steeply past his lower abdomen, open shorts, thighs, and feet. desktop edges and table legs appear along the side margins while the carpet fills the center. the woman kneels on the carpet directly below the viewer between his feet. her mouth seals around the short centered large penis. both hands clasp the base under her lips. her hair crown, forehead, shoulders, and knees are visible from above. chair wheels and table bases mark the floor depth around her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "head_crown_forehead_vertical_read",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["head_crown", "forehead_visibility", "vertical_read"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position. viewer looks down over his lower abdomen, open shorts, thighs, and feet to the carpet directly below. the woman's hair crown is the topmost visible part of her head, with forehead, eyes, nose bridge, shoulders, hands, and knees stacked underneath from the steep angle. her mouth seals around the short centered large penis rising from the lower frame. both hands hold the base. the floor plane dominates the image with carpet weave, chair wheels, desk-leg feet, table bases, and seams surrounding her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "office_lane_floor_perspective",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["office_lane", "floor_perspective", "workspace_continuity"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV oral position in a narrow coworking floor lane. viewer looks steeply down from his abdomen. open shorts, thighs, feet, and lower torso anchor the lower foreground. a lane of carpet, chair caster wheels, table bases, and desk legs extends behind the kneeling woman. the woman is directly below the viewer in the lane, centered between his feet. her mouth seals around the short centered large penis. both hands stack at the base under her lips. hair crown, forehead, shoulders, and knees are readable from above. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "low_head_high_floor_ratio",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["floor_ratio", "head_below_viewer", "verticality"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs, and feet sit at the bottom edge. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"probes": [
|
||||
{
|
||||
"id": "baseline_ref_folder",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 27,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_40bd9ea31595497eacabd4bf08fc5088.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "floor_plane_mouth_under_torso",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 28,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_b6525cf541d44c34828c7fe19068425b.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "caster_wheel_floor_grid",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 29,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_4f1bc1b6a655401d92d883043b7af76a.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "desk_leg_rows_floor_depth",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 30,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_5606d28cf60e41c7a894b3e4e667cd88.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "viewer_feet_gate_head_center",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 31,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_15c47d49bd344721a598c3ab024a6278.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "phone_snapshot_abdomen_down",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 32,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_d4f87fa4e43548d7af12af13a78e1cb3.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "straight_down_carpet_tiles",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 33,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_3a84cc08518447a0a9b302d9bfb55191.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "mouth_below_navel_eye_contact",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 34,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_984845bbd30449e3a801b40917eec708.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "kneeling_between_shoes_floor_map",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 35,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_dfc98e75d16946f89e9b96eed2675993.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "short_column_contact_scale",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 36,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_6bb47f36d12740a8aa9a815801988b68.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "woman_tank_top_owned_visibility",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 37,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_9f6fb18a5d574237910a15847cf762ca.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "desk_edges_side_floor_anchor",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 38,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_7e4432d41e6c4defb44fa8582c127764.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "head_crown_forehead_vertical_read",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 39,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_31c14beb5cee41a29e15a4d2d48218b3.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "office_lane_floor_perspective",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 40,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_3eb8c5b4cc6648b7ab0fd5555488d657.png",
|
||||
"returned_seed": 238365845574312
|
||||
},
|
||||
{
|
||||
"id": "low_head_high_floor_ratio",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 41,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_9f12349bc10e4dce908babe161200814.png",
|
||||
"returned_seed": 238365845574312
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"seed": 238365845574313,
|
||||
"channel_out": "sxcp_eval_out",
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"subject_id": "atlas_refine_same_woman_001",
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft",
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"selection": {
|
||||
"purpose": "second-pass top-view axis refinement using folder baseline and first MCP batch evidence",
|
||||
"sampler_seed_role": "second fixed sampler seed for repeatable matrix comparison"
|
||||
},
|
||||
"probes": [
|
||||
{
|
||||
"id": "baseline_ref_folder",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["baseline", "folder_reference"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir-angle standing male POV top-view oral position. viewer looks almost straight down from his torso toward the floor. nearby carpet floor plane dominates the image. viewer abdomen, shorts, thighs, and feet frame the lower foreground. large penis is a short centered vertical column. the woman kneels directly below the viewer between his feet. her mouth seals around the centered large penis. one hand wraps the base. hair crown, forehead, shoulders, hands, and knees are visible from above. desk legs, chair wheels, carpet texture, and floor seams act as top-down office anchors. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "floor_plane_mouth_under_torso",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["floor_plane_priority", "mouth_under_torso", "contact_preservation"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Floor-plane-priority standing male POV oral position. viewer stands above the kneeling woman and looks down from his abdomen. open shorts, thighs, feet, and lower abdomen frame the bottom. the carpet plane is the main background surface, filled with woven texture, floor seams, desk-leg feet, and caster wheels. the large penis rises from the lower center as a foreshortened vertical cylinder. the woman kneels in the middle of the carpet plane, mouth sealed around the centered large penis. both hands wrap around the base, shoulders and knees visible below her hair crown. her dark eyes angle up toward the viewer. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "caster_wheel_floor_grid",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["caster_wheels", "top_down_grid", "office_floor_anchor"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position on office carpet. the camera looks down past the viewer's abdomen, open shorts, thighs, and feet. chair caster wheels, desk-leg feet, table bases, and carpet seams form a visible grid across the floor. the woman kneels centered inside that floor grid between the viewer's shoes. her hair crown, forehead, shoulders, hands, and knees are seen from above. her mouth seals around the foreshortened centered large penis rising from the lower frame. both hands stack at the base under her lips. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "desk_leg_rows_floor_depth",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["desk_leg_rows", "vertical_depth", "floor_background"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position between coworking desk rows. viewer looks steeply downward from his torso. open shorts, thighs, feet, and lower abdomen hold the lower edge. repeated desk legs and chair wheels recede across the carpet behind the kneeling woman. the floor plane fills most of the image. the woman kneels directly below the viewer, centered between his feet. her mouth covers the short foreshortened large penis at the exact center. both hands hold the base, elbows tucked close, shoulders visible below her hair crown. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "viewer_feet_gate_head_center",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["viewer_feet_frame", "head_centering", "standing_height"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep standing male POV oral position. viewer's bare feet and open shorts create a gate around the bottom of the frame. the camera looks down between his thighs onto the carpet. the woman's head is centered directly below the viewer's pelvis between his feet. her hair crown and forehead face the camera from above while her dark eyes look upward. a short vertical large penis rises from the lower center into her sealed mouth. both hands wrap the base under her lips. floor seams, chair wheels, and desk legs surround her knees on the carpet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "phone_snapshot_abdomen_down",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["phone_like_snapshot", "abdomen_down_angle", "floor_reference"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Phone-like standing male POV oral snapshot from above. the viewer looks down from chest and abdomen height. lower abdomen, open shorts, thighs, and feet occupy the near bottom edge. the carpet below is clear and detailed, with chair wheels, desk-leg feet, and table bases placed around the woman. she kneels directly underneath the viewer between his feet. her mouth seals around the centered short large penis. both hands clasp the base and her shoulders sit below her hair crown. her dark almond eyes glance upward into the camera. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "straight_down_carpet_tiles",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["straight_down_axis", "carpet_tiles", "top_view"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nearly straight-down standing male POV oral position. the viewer's abdomen, shorts, thighs, and feet frame a steep view onto carpet tiles. the carpet tile seams run beneath the kneeling woman and make the camera angle read vertical. she kneels below the viewer with knees spread on the carpet. the short centered large penis points upward from the lower frame into her sealed mouth. both hands hold the base at the viewer's pelvis line. hair crown, forehead, shoulders, hands, and knees are all visible from above. desk legs and chair wheels sit flat on the floor around her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "mouth_below_navel_eye_contact",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["mouth_below_navel", "eye_contact", "contact_preservation"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position with eye contact. viewer looks down past his navel, open shorts, thighs, and feet. the woman's mouth sits directly below the viewer's navel line on the carpet plane. her lips seal around the centered foreshortened large penis and both hands clasp the base. her face tilts upward, dark almond eyes looking into the camera. the hair crown, forehead, shoulders, hands, and knees remain readable from above. carpet texture, desk legs, chair wheels, and table bases flatten into the background floor grid. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "kneeling_between_shoes_floor_map",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["kneeling_between_feet", "floor_map", "standing_reference"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position. the viewer's feet sit near the lower left and lower right edges like fixed markers on the carpet. his open shorts, thighs, and lower abdomen occupy the bottom edge. the woman kneels between those feet, directly under the viewer's pelvis. her head, shoulders, hands, and knees fit inside the visible floor map. the short centered large penis rises from the lower center into her sealed mouth. both hands wrap the base under her lips. office desk legs, caster wheels, carpet seams, and table bases show the flat floor orientation. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "short_column_contact_scale",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["contact_scale", "foreshortened_column", "anatomy_length_control"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep standing male POV oral position with foreshortened contact scale. viewer looks down from his abdomen to the carpet. lower abdomen, open shorts, thighs, and feet frame the bottom foreground. the large penis reads as a short centered vertical column because the camera is directly above it. the woman kneels below the viewer and seals her mouth around the centered tip. both hands clasp low at the base near the viewer's pelvis. her hair crown, forehead, eyes, shoulders, and knees are visible from the top angle. floor seams, desk legs, caster wheels, and chair bases stay flat around her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "woman_tank_top_owned_visibility",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["owned_clothing", "visible_top", "pose_preservation"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. Standing male POV top-view oral position. viewer looks down from his abdomen with open shorts, thighs, and feet framing the lower edge. the woman kneels directly below the viewer on the carpet between his feet. her tank top shoulders and neckline are visible below her hair crown from above. her mouth seals around the centered short large penis. both hands wrap the base under her lips. carpet texture, floor seams, desk legs, caster wheels, and table bases surround her knees and anchor the top-down office floor. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "desk_edges_side_floor_anchor",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["desk_edges", "side_background", "floor_angle"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position beside coworking desks. viewer looks down steeply past his lower abdomen, open shorts, thighs, and feet. desktop edges and table legs appear along the side margins while the carpet fills the center. the woman kneels on the carpet directly below the viewer between his feet. her mouth seals around the short centered large penis. both hands clasp the base under her lips. her hair crown, forehead, shoulders, and knees are visible from above. chair wheels and table bases mark the floor depth around her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "head_crown_forehead_vertical_read",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["head_crown", "forehead_visibility", "vertical_read"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position. viewer looks down over his lower abdomen, open shorts, thighs, and feet to the carpet directly below. the woman's hair crown is the topmost visible part of her head, with forehead, eyes, nose bridge, shoulders, hands, and knees stacked underneath from the steep angle. her mouth seals around the short centered large penis rising from the lower frame. both hands hold the base. the floor plane dominates the image with carpet weave, chair wheels, desk-leg feet, table bases, and seams surrounding her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "office_lane_floor_perspective",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["office_lane", "floor_perspective", "workspace_continuity"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV oral position in a narrow coworking floor lane. viewer looks steeply down from his abdomen. open shorts, thighs, feet, and lower torso anchor the lower foreground. a lane of carpet, chair caster wheels, table bases, and desk legs extends behind the kneeling woman. the woman is directly below the viewer in the lane, centered between his feet. her mouth seals around the short centered large penis. both hands stack at the base under her lips. hair crown, forehead, shoulders, and knees are readable from above. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "low_head_high_floor_ratio",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["floor_ratio", "head_below_viewer", "verticality"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs, and feet sit at the bottom edge. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"seed": 238365845574313,
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"probes": [
|
||||
{
|
||||
"id": "baseline_ref_folder",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 42,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_ee2c4c4e134441e99050c066ce3e3ccd.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "floor_plane_mouth_under_torso",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 43,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_fe09b3d132004f26ab848f3edb21d8a0.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "caster_wheel_floor_grid",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 44,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_15a44ffbaa5647f2aa93f15a5421bf68.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "desk_leg_rows_floor_depth",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 45,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_f459a958fbfa4cf4b3268b92c3f5ad4e.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "viewer_feet_gate_head_center",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 46,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_13751f4e0fe54330aceaedb8fa260a16.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "phone_snapshot_abdomen_down",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 47,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_387428c4386d4de68e95af66d4afc887.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "straight_down_carpet_tiles",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 48,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_47825cbcc88946ca939b3b3e348a71b1.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "mouth_below_navel_eye_contact",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 49,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_0d123f63cb214049ac6b1f14b2e41c59.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "kneeling_between_shoes_floor_map",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 50,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_b6ea951bbd2c4210b38b2f6d1c2bebb7.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "short_column_contact_scale",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 51,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_ce9b7fdf8d5a49feae653bfec93879f8.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "woman_tank_top_owned_visibility",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 52,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_20edcebe86424ec0b8fa9e6f027f725f.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "desk_edges_side_floor_anchor",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 53,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_d8ef57b7ca4c4b04a8c55bbcf2c626a0.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "head_crown_forehead_vertical_read",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 54,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_90b9792203b74925abc913ddee696809.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "office_lane_floor_perspective",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 55,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_bd754c0e15514ffc876b6a746d01268b.png",
|
||||
"returned_seed": 238365845574313
|
||||
},
|
||||
{
|
||||
"id": "low_head_high_floor_ratio",
|
||||
"prompt_order": "subject_first",
|
||||
"turn": 56,
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_08635c3c3c354716a837d0945b4b70c6.png",
|
||||
"returned_seed": 238365845574313
|
||||
}
|
||||
]
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_out": "sxcp_eval_out",
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"subject_id": "atlas_refine_same_woman_001",
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft",
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"selection": {
|
||||
"purpose": "reduce office-background density while preserving the strongest straight-down floor-plan top-view oral wording",
|
||||
"sampler_seed_role": "fixed sampler seed for comparison against straight_down_floorplan result"
|
||||
},
|
||||
"probes": [
|
||||
{
|
||||
"id": "straight_down_floorplan_control",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["control", "straight_down_prefix", "floorplan_scene_tail"],
|
||||
"evidence": {
|
||||
"prior_image": "/media/unraid/comfyui/output/agent_bridge/img_02410cd85ef14ceaa160cca11116f5cb.png"
|
||||
},
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman."
|
||||
},
|
||||
{
|
||||
"id": "sparse_carpet_plane",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["sparse_floor", "background_density", "floor_ratio"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. a broad sparse carpet plane surrounds the kneeling woman. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Coworking lounge evidence appears as carpet texture, carpet tile seams, two cropped chair caster wheels near the far edge, and one cropped table foot at the margin."
|
||||
},
|
||||
{
|
||||
"id": "empty_carpet_halo",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["empty_floor_area", "background_density", "top_view"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen onto a mostly open carpet floor. a large empty carpet halo surrounds the woman from above. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Coworking lounge floor cues are sparse carpet seams and small cropped furniture feet at the outer edge."
|
||||
},
|
||||
{
|
||||
"id": "edge_cropped_office_marks",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["edge_crops", "background_density", "workspace_floor"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen. the carpet floor fills the central image. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Office furniture appears as cropped edge marks: partial desk feet, partial chair caster wheels, and small table-base slivers along the frame margins."
|
||||
},
|
||||
{
|
||||
"id": "carpet_tile_map",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["carpet_grid", "floor_map", "reduced_background"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet tile map takes most of the frame. carpet seams form a flat square grid around the kneeling woman. her head stays low in the center of that carpet grid between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Coworking context reads through a few cropped caster wheels and desk feet at the grid edges."
|
||||
},
|
||||
{
|
||||
"id": "floor_plane_is_background",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["floor_as_background", "scene_reconciliation", "top_view"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen. the carpet floor plane is the background. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. The coworking lounge appears through flat floor details: carpet weave, carpet seams, small caster wheels, desk feet, and cropped table-base edges."
|
||||
},
|
||||
{
|
||||
"id": "minimal_floorplan_compact",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["short_prompt", "noise_reduction", "sparse_floor"],
|
||||
"text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen onto the carpet floor. the woman kneels in the center of a sparse carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base. hair crown, forehead, shoulders, hands, and knees read from above. Sparse coworking floor cues: carpet seams, cropped caster wheels, cropped desk feet."
|
||||
},
|
||||
{
|
||||
"id": "tight_vertical_subject_sparse_floor",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["tight_crop", "sparse_floor", "contact_priority"],
|
||||
"text": "Tight straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen. the kneeling woman and centered contact fill the lower half while sparse carpet fills the upper half. her head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Coworking lounge floor cues stay at the margins as carpet seams, caster wheels, and desk feet."
|
||||
},
|
||||
{
|
||||
"id": "atlas_like_floor_dominant",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["atlas_like_floor_ratio", "sparse_floor", "top_view"],
|
||||
"text": "Atlas-like straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the floor plane dominates the composition. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Sparse coworking floor marks surround her: carpet weave, carpet seams, cropped chair wheels, and cropped table feet."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
# Blowjob Top-View 1024 Reference Pool Cue Expansion
|
||||
|
||||
Date: 2026-07-01
|
||||
|
||||
Canonical atlas folder:
|
||||
|
||||
```text
|
||||
/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/blowjob_top_view
|
||||
```
|
||||
|
||||
Supplemental raw cue-expansion folder:
|
||||
|
||||
```text
|
||||
/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/1.original/blowjob_top_view_1024
|
||||
```
|
||||
|
||||
Folder state:
|
||||
|
||||
- canonical folder: 17 PNG images
|
||||
- supplemental raw folder: 27 PNG images
|
||||
- all images are `1024x1024`
|
||||
- raw-pool contact sheet created for review at `/tmp/blowjob_top_view_1024_contact.jpg`
|
||||
|
||||
Reference-pool report command:
|
||||
|
||||
```bash
|
||||
python tools/krea2_atlas_refine_manifest.py --print-reference-pool-report --variant-key pov_blowjob_top_down_vertical_shaft --reference-pool-folder 1.original/blowjob_top_view_1024
|
||||
```
|
||||
|
||||
Cue-review sheet command:
|
||||
|
||||
```bash
|
||||
python tools/krea2_atlas_refine_manifest.py --print-reference-cue-review-sheet --variant-key pov_blowjob_top_down_vertical_shaft --reference-pool-folder 1.original/blowjob_top_view_1024
|
||||
```
|
||||
|
||||
The cue-review sheet currently creates 27 blank review items: 17 canonical
|
||||
curated references, all matched to raw counterparts by image id, plus 10
|
||||
raw-only supplemental extras. Canonical rows include a
|
||||
`reference_images_template`; raw-only rows leave that template empty until a
|
||||
human review decides whether the extra frame is only cue-mining evidence or
|
||||
deserves promotion into the curated catalog.
|
||||
|
||||
Filled cue-review sheet to sidecar-candidate draft command:
|
||||
|
||||
```bash
|
||||
python tools/krea2_atlas_refine_manifest.py --print-reference-cue-candidate-draft --reference-cue-review-sheet-json /tmp/sxcp-reference-cue-review-filled.json
|
||||
```
|
||||
|
||||
The candidate draft emits copyable `prompt_variant` objects only for reviewed
|
||||
canonical rows with positive cues and a filled prompt-variant id. It skips
|
||||
raw-only extras, noisy option/meta/negative cue wording, blank rows, and
|
||||
duplicate ids so the raw pool cannot silently become promoted generator truth.
|
||||
|
||||
Candidate draft to same-stem sidecar authoring draft:
|
||||
|
||||
```bash
|
||||
python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-reference-cue-sidecar-author-draft --reference-cue-candidate-draft-json /tmp/sxcp-reference-cue-candidate-draft.json --variant-key pov_blowjob_top_down_vertical_shaft
|
||||
```
|
||||
|
||||
Validate and apply only after confirming the target baseline deck is the one to
|
||||
test:
|
||||
|
||||
```bash
|
||||
python tools/krea2_atlas_refine_manifest.py --validate-reference-cue-sidecar-author-draft --reference-cue-sidecar-author-draft-json /tmp/sxcp-reference-cue-sidecar-author-draft.json
|
||||
python tools/krea2_atlas_refine_manifest.py --apply-reference-cue-sidecar-author-draft --reference-cue-sidecar-author-draft-json /tmp/sxcp-reference-cue-sidecar-author-draft.json --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine
|
||||
```
|
||||
|
||||
Apply writes unscored sidecar prompt variants and checks the source prompt hash.
|
||||
The next required action is fixed-seed MCP rendering and scoring, not catalog
|
||||
promotion.
|
||||
|
||||
## Why This Pool Matters
|
||||
|
||||
The canonical folder is the preferred source for curated atlas references. The
|
||||
supplemental raw folder shows the same pose family with more images, so it can
|
||||
drive cue expansion when repeated visual axes are visible across references. It
|
||||
should not be treated as proof that our current generated prompt can preserve
|
||||
the same subject, coworking workspace, clothing ownership, or anatomy behavior.
|
||||
|
||||
Use this pool upstream of sidecar authoring:
|
||||
|
||||
1. Pick the nearest atlas cluster for the target variant, preferring the
|
||||
canonical `blowjob_top_view/` path when the frame exists there.
|
||||
2. Extract positive cue axes from repeated visual evidence.
|
||||
3. Write small `append_cues` against the current generated baseline and store
|
||||
the nearest atlas targets in `reference_images`.
|
||||
4. Test with fixed sampler seeds through the MCP batch path.
|
||||
5. Score generated results against the nearest atlas cluster and the current
|
||||
same-subject/workspace baseline before promotion.
|
||||
6. Keep raw-only extras in the cue-review sheet until repeated generated
|
||||
evidence proves the cue belongs in a sidecar or catalog entry.
|
||||
7. Convert only reviewed canonical rows to candidate sidecar snippets; then
|
||||
render and score them before seed selection.
|
||||
8. Use the sidecar authoring draft to attach reviewed candidates to a baseline
|
||||
deck with prompt-hash drift protection.
|
||||
|
||||
## Observed Cue Axes
|
||||
|
||||
- `camera_pitch`: straight-down vertical, high oblique top-down, and tilted
|
||||
top-down variants all appear in the pool.
|
||||
- `support_plane`: white lounge/chair surfaces, pale floor, carpet/rug, bed or
|
||||
blanket surfaces, wood floor, tile floor, and outdoor ground appear as visible
|
||||
plane anchors.
|
||||
- `viewer_foreground_amount`: some refs use only a lower torso edge, while
|
||||
others show more thighs, feet, waistband, or abdomen mass.
|
||||
- `partner_upper_body_stack`: the strongest top-view refs place face, eyes or
|
||||
eyelids, hair crown, shoulders, upper chest or neckline, and hand contact as
|
||||
the main partner stack.
|
||||
- `hand_placement`: one hand at the base, both hands supporting the base, hands
|
||||
on floor/support, and hands on the viewer/body edge appear as alternate frame
|
||||
cues.
|
||||
- `eye_direction`: direct eye contact, eyelids lowered, and open-mouth/gaze-up
|
||||
variants appear as expression axes.
|
||||
- `clothing_anchor`: fitted tops, straps, shirts, or visible necklines can help
|
||||
anchor upper-body geometry when the crop supports them.
|
||||
- `floor_furniture_evidence`: cropped support edges, floor seams, rugs, chair or
|
||||
furniture edges, and outdoor objects can carry scene identity without forcing
|
||||
deep room perspective.
|
||||
|
||||
## Current Top-View Rule From This Pool
|
||||
|
||||
For coworking prompt tests, translate workspace context into floor/support-plane
|
||||
evidence and keep the shaft/contact line as the first pose anchor. The
|
||||
user-highlighted atlas-22-style direction is:
|
||||
|
||||
```text
|
||||
Straight-down male POV oral close-up. The centered shaft and mouth contact form
|
||||
the main vertical axis from the lower foreground to the woman's face. The
|
||||
woman's face, eyelids, hair crown, shoulders, upper chest, neckline, and one
|
||||
hand stack around the shaft-contact axis. Viewer thighs and feet frame the
|
||||
lower side edges. Tucked knees remain small side shapes on the floor. The
|
||||
background reads as a flat pale floor and one cropped white lounge chair
|
||||
surface, with shallow top-down room depth.
|
||||
```
|
||||
|
||||
Manual same-seed calibration on 2026-07-01 confirms the anchor order. The
|
||||
sidecars for `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00135_.png`
|
||||
through `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00139_.png`
|
||||
performed best when the prompt started with straight-down POV, then the shaft,
|
||||
then the partner stack directly below the shaft, then mouth contact, and ended
|
||||
with a sparse flat-floor background. Treat abdomen and room-depth cues as
|
||||
secondary evidence; they should follow the shaft-contact axis instead of
|
||||
leading the composition.
|
||||
|
||||
Because this finding depends on word order and removes the deep coworking-room
|
||||
tail, test it as exact replacement text instead of appended cues. The validated
|
||||
dry-run batch is:
|
||||
|
||||
```text
|
||||
/tmp/sxcp_top_view_oral_shaft_anchor_exact_batch.json
|
||||
```
|
||||
|
||||
It keeps the original baseline probe plus two exact-text candidates:
|
||||
|
||||
- `atlas22_shaft_contact_upper_stack_floor_plane`
|
||||
- `atlas27_shaft_axis_between_feet_floor_anchors`
|
||||
|
||||
For clothed variants, keep clothing subject-owned:
|
||||
|
||||
```text
|
||||
The woman wears a fitted white ribbed tank top; the tank-top neckline and
|
||||
shoulders remain visible from above.
|
||||
```
|
||||
|
||||
## Promotion Guard
|
||||
|
||||
Do not copy all 27 supplemental references into the live catalog variant. Keep
|
||||
the catalog reference list curated, then use the raw pool to choose cue axes and
|
||||
nearest visual targets. A generated candidate still needs fixed-seed image
|
||||
evidence and visual scores for pose ownership, workspace continuity, clothing
|
||||
visibility, subject identity, expression/eye control, anatomy/proportion, and
|
||||
prompt noise before it can become seedable. Sidecar cue variants should carry
|
||||
nearest visual targets like:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "shaft_contact_upper_stack_floor_support",
|
||||
"text": "A same-subject straight-down male POV oral close-up. The centered shaft and mouth contact form the main vertical axis from the lower foreground to the woman's face. The woman's face, eyelids, hair crown, shoulders, upper chest, neckline, and one hand stack around the shaft-contact axis. The background reads as a flat pale floor plane with shallow overhead room depth.",
|
||||
"reference_images": [
|
||||
"blowjob_top_view/22_blowjob_top_view.png"
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"seed": 238365845574312,
|
||||
"channel_out": "sxcp_eval_out",
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"subject_id": "atlas_refine_same_woman_001",
|
||||
"variant_key": "pov_footjob_frontal_sole_stroke",
|
||||
"source_entry_id": "pov_footjob_frontal_sole_stroke_00001",
|
||||
"source_stem": "pov_footjob_frontal_sole_stroke_00001_",
|
||||
"selection": {
|
||||
"purpose": "controlled same-pose micro-position cue exploration for seedable atlas alternatives",
|
||||
"sampler_seed_role": "fixed sampler seed for first-pass micro-position comparison"
|
||||
},
|
||||
"probes": [
|
||||
{
|
||||
"id": "baseline_ref_folder",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["baseline", "folder_reference"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. two large overlapping soles dominate the lower center foreground. inner arches press inward from both sides around the upright large penis. toes curl around both edges. narrow visible strip of large penis and glans rises between the compressed feet. woman's face and torso stay visible behind the large foreground feet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "soles_closer_to_lens",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["foot_distance", "foreground_scale", "sole_dominance"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. two large soles push closer to the lens and dominate the lower center foreground. inner arches press inward around the upright large penis. toes curl around both sides near the glans. a narrow centered strip of large penis rises between the compressed feet. woman's face and torso remain visible behind the enlarged foreground soles. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "soles_lower_more_column_visible",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["foot_distance", "contact_visibility", "column_visibility"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. both soles press lower on the shaft and leave more of the upright large penis visible above the feet. inner arches squeeze inward at the middle. toes curl around the sides below the glans. woman's face and torso stay visible behind the paired feet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "toes_wrap_over_tip",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["toe_position", "glans_contact", "micro_contact"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. two foreground soles press together around the upright large penis. her toes curl over the top edges near the glans. inner arches squeeze the shaft from both sides. the glans peeks between the curled toes and compressed soles. woman's face and torso stay visible behind the large feet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "left_sole_forward_right_sole_back",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["foot_asymmetry", "same_pose_frame", "depth_variation"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. her left sole sits slightly closer to the lens while her right sole sits a little farther back. the paired arches still squeeze around the upright large penis. toes curl along both edges. a centered strip of large penis remains visible between the offset feet. woman's face and torso stay visible behind the asymmetrical foreground soles. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "right_sole_forward_left_sole_back",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["foot_asymmetry", "same_pose_frame", "depth_variation"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. her right sole sits slightly closer to the lens while her left sole sits a little farther back. the paired arches squeeze around the upright large penis. toes curl along both edges. a centered strip of large penis remains visible between the offset feet. woman's face and torso stay visible behind the asymmetrical foreground soles. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "v_angle_arch_press",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["foot_angle", "arch_pressure", "contact_shape"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. her soles form a tight V shape around the upright large penis. inner arches press inward at the center and the toes angle outward along the top edges. a narrow vertical strip of large penis and glans rises between the angled soles. woman's face and torso stay visible behind the foot frame. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "flat_parallel_soles",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["foot_angle", "parallel_contact", "controlled_variant"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. both soles face the camera almost flat and parallel. the upright large penis is centered between the parallel soles. inner arches press evenly inward from left and right. toes curl around the upper edges while her face and torso stay visible behind the large foreground feet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "ankles_wide_soles_squeeze_center",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["ankle_position", "leg_spread", "center_squeeze"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. her ankles stay wide apart while the soles angle inward to squeeze the upright large penis at the center. toes curl around both sides. the compressed feet frame a narrow visible strip of shaft and glans. woman's face, torso, and spread thighs stay visible behind the large foot foreground. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "knees_higher_foot_frame",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["knee_position", "foot_frame_height", "body_position"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with knees lifted higher beside her torso. the raised knees frame the large foreground soles. her soles press together around the upright large penis at the center. toes curl around the sides near the glans. woman's face and torso stay visible above the higher knee frame. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "woman_looking_at_viewer",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["gaze_control", "facial_expression", "pose_preservation"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. two large overlapping soles dominate the lower center foreground. inner arches press inward from both sides around the upright large penis. toes curl around both edges. narrow visible strip of large penis and glans rises between the compressed feet. woman's face and torso stay visible behind the large feet, her dark almond eyes looking toward the viewer with a focused soft expression. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "woman_looking_down_at_feet",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["gaze_control", "facial_expression", "pose_preservation"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. two large overlapping soles dominate the lower center foreground. inner arches press inward from both sides around the upright large penis. toes curl around both edges. narrow visible strip of large penis and glans rises between the compressed feet. woman's face and torso stay visible behind the large feet, her dark eyes looking down at her own feet with a calm focused expression. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "woman_white_tank_top_visible",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["owned_clothing", "clothing_visibility", "pose_preservation"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top visible behind the raised soles. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. two large overlapping soles dominate the lower center foreground. inner arches press inward from both sides around the upright large penis. toes curl around both edges. narrow visible strip of large penis and glans rises between the compressed feet. woman's face, tank top, and torso stay visible behind the large foreground feet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "seat_edge_lounge_support",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["workspace_interaction", "support_surface", "scene_continuity"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position on a coworking lounge seat. viewer reclines with thighs framing the lower foreground. woman sits opposite on the seat edge facing him with legs open toward the camera. the cushion edge appears below her hips while desk rows, laptop tables, chair wheels, and glass partitions stay in the background. two large overlapping soles dominate the lower center foreground. inner arches squeeze around the upright large penis and toes curl around both edges. woman's face and torso stay visible behind the raised feet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
},
|
||||
{
|
||||
"id": "low_table_side_anchor",
|
||||
"prompt_order": "subject_first",
|
||||
"cue_axes": ["workspace_interaction", "side_anchor", "scene_continuity"],
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position beside a low coworking table. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. a low laptop table and chair wheels sit along the side of the frame while the action stays centered. two large overlapping soles dominate the lower center foreground. inner arches squeeze around the upright large penis. toes curl around both edges and a narrow visible strip of shaft and glans rises between the feet. woman's face and torso stay visible behind the foreground soles. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"schema": "sxcp_atlas_reference_cue_review_sheet_v1",
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft",
|
||||
"review_items": [
|
||||
{
|
||||
"id": "22",
|
||||
"role": "catalog_reference",
|
||||
"canonical_image": "blowjob_top_view/22_blowjob_top_view.png",
|
||||
"supplemental_image": "1.original/blowjob_top_view_1024/22.png",
|
||||
"reference_images_template": [
|
||||
"blowjob_top_view/22_blowjob_top_view.png"
|
||||
],
|
||||
"cue_axes": {
|
||||
"contact_depth": "shaft_contact_axis_centered_from_lower_foreground_to_mouth",
|
||||
"hand_position": "one_hand_base_contact",
|
||||
"foot_position": "",
|
||||
"body_angle": "compact_kneeling_upper_body_stack_below_shaft",
|
||||
"camera_height": "straight_down_overhead_shaft_first",
|
||||
"workspace_surface": "flat_pale_floor_with_cropped_support_edge_shallow_depth",
|
||||
"clothing_visibility": "upper_body_neckline_anchor",
|
||||
"expression_eye_detail": "upward_face_or_lowered_eyelids_visible_from_above",
|
||||
"anatomy_shape_detail": "small_tucked_knees_beside_upper_body_stack"
|
||||
},
|
||||
"observed_positive_cues": [
|
||||
"the centered shaft and mouth contact form the main vertical axis from the lower foreground to the woman's face",
|
||||
"the woman's face, eyelids, hair crown, shoulders, upper chest, neckline, and one hand stack around the shaft-contact axis",
|
||||
"a flat pale floor plane and one cropped support edge fill the background as shallow overhead room evidence"
|
||||
],
|
||||
"rejected_cues": [],
|
||||
"review_notes": "Atlas 22 anchors the strongest shaft-contact axis, upper-body stack, and shallow top-down support-plane read.",
|
||||
"prompt_variant_template": {
|
||||
"id": "atlas22_shaft_contact_upper_stack_floor_plane",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Straight-down male POV oral close-up. The centered large penis and mouth contact form the main vertical axis from the lower foreground to the woman's face. The woman kneels directly below the penis. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, upper chest, neckline, and one hand stack around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Tucked knees sit small on the floor beside the upper-body stack. Background reads as a flat pale floor plane with one cropped support edge and shallow overhead room depth.",
|
||||
"append_cues": [],
|
||||
"reference_images": [
|
||||
"blowjob_top_view/22_blowjob_top_view.png"
|
||||
],
|
||||
"cue_axes": {
|
||||
"contact_depth": null,
|
||||
"hand_position": null,
|
||||
"foot_position": null,
|
||||
"body_angle": null,
|
||||
"camera_height": null,
|
||||
"workspace_surface": null,
|
||||
"clothing_visibility": null,
|
||||
"expression_eye_detail": null,
|
||||
"anatomy_shape_detail": null
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": null,
|
||||
"generator_seed": null,
|
||||
"atlas_cue_seed": null,
|
||||
"micro_position_seed": null,
|
||||
"workspace_seed": null
|
||||
},
|
||||
"notes": "Reviewed from canonical atlas 22; test as shaft-contact axis, upper-body stack, and sparse floor-plane cue."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "27",
|
||||
"role": "catalog_reference",
|
||||
"canonical_image": "blowjob_top_view/27_blowjob_top_view.png",
|
||||
"supplemental_image": "1.original/blowjob_top_view_1024/27.png",
|
||||
"reference_images_template": [
|
||||
"blowjob_top_view/27_blowjob_top_view.png"
|
||||
],
|
||||
"cue_axes": {
|
||||
"contact_depth": "shaft_contact_axis_centered_between_viewer_feet",
|
||||
"hand_position": "one_hand_base_contact",
|
||||
"foot_position": "viewer_feet_lower_side_anchors",
|
||||
"body_angle": "directly_below_viewer_between_feet",
|
||||
"camera_height": "standing_nadir_overhead_shaft_first",
|
||||
"workspace_surface": "floor_texture_with_cropped_furniture_edge_anchors",
|
||||
"clothing_visibility": "upper_body_clothing_visible_from_above",
|
||||
"expression_eye_detail": "face_looking_up_from_below_camera",
|
||||
"anatomy_shape_detail": "head_and_shoulders_closer_than_knees"
|
||||
},
|
||||
"observed_positive_cues": [
|
||||
"the centered shaft rises from the lower foreground and points directly to the woman's mouth between the viewer's feet",
|
||||
"viewer thighs and feet frame the lower side edges while the shaft-contact line stays centered",
|
||||
"cropped desk legs, chair wheels, carpet texture, and floor seams sit near the frame edges as top-down workspace anchors"
|
||||
],
|
||||
"rejected_cues": [],
|
||||
"review_notes": "Atlas 27 carries stronger standing-nadir verticality, shaft-first alignment, and edge furniture anchors.",
|
||||
"prompt_variant_template": {
|
||||
"id": "atlas27_shaft_axis_between_feet_floor_anchors",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing nadir male POV oral close-up. The centered large penis rises from the lower foreground and points directly to the woman's mouth between the viewer's feet. The woman kneels directly below the viewer between his feet. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, hands, and knees are visible from above around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Cropped desk legs, chair wheels, carpet texture, and floor seams sit near the frame edges as top-down workspace anchors.",
|
||||
"append_cues": [],
|
||||
"reference_images": [
|
||||
"blowjob_top_view/27_blowjob_top_view.png"
|
||||
],
|
||||
"cue_axes": {
|
||||
"contact_depth": null,
|
||||
"hand_position": null,
|
||||
"foot_position": null,
|
||||
"body_angle": null,
|
||||
"camera_height": null,
|
||||
"workspace_surface": null,
|
||||
"clothing_visibility": null,
|
||||
"expression_eye_detail": null,
|
||||
"anatomy_shape_detail": null
|
||||
},
|
||||
"seed_metadata": {
|
||||
"sampler_seed": null,
|
||||
"generator_seed": null,
|
||||
"atlas_cue_seed": null,
|
||||
"micro_position_seed": null,
|
||||
"workspace_seed": null
|
||||
},
|
||||
"notes": "Reviewed from canonical atlas 27; test as standing-nadir shaft axis between the viewer's feet."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"baseline_probe_id": "pov_blowjob_top_down_vertical_shaft_00001__baseline",
|
||||
"blocked_count": 1,
|
||||
"candidate_count": 2,
|
||||
"candidates": [
|
||||
{
|
||||
"analysis_notes": "Turn 90 strongly improves shaft/contact verticality and upper-body stack over baseline. It loses most coworking workspace identity, so keep it as shaft-anchor evidence rather than seedable workspace-continuity evidence.",
|
||||
"blockers": [
|
||||
"workspace_continuity=partial"
|
||||
],
|
||||
"cue_axes": {
|
||||
"anatomy_shape_detail": "small_tucked_knees_beside_upper_body_stack",
|
||||
"body_angle": "compact_kneeling_upper_body_stack_below_shaft",
|
||||
"camera_height": "straight_down_overhead_shaft_first",
|
||||
"clothing_visibility": "upper_body_neckline_anchor",
|
||||
"contact_depth": "shaft_contact_axis_centered_from_lower_foreground_to_mouth",
|
||||
"expression_eye_detail": "upward_face_or_lowered_eyelids_visible_from_above",
|
||||
"foot_position": null,
|
||||
"hand_position": "one_hand_base_contact",
|
||||
"workspace_surface": "flat_pale_floor_with_cropped_support_edge_shallow_depth"
|
||||
},
|
||||
"decision": "rejected",
|
||||
"id": "pov_blowjob_top_down_vertical_shaft_00001__atlas22_shaft_contact_upper_stack_floor_plane",
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_cca7cd5c69894cbb899f94b172faa5d1.png",
|
||||
"prompt_order": "subject_first",
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "atlas22_shaft_contact_upper_stack_floor_plane",
|
||||
"tested_text_sha256": "49495cc80d3ea87a49b46b20aa716b4e07ebc3179617b6252c310bcde7788848"
|
||||
},
|
||||
"prompt_variant_id": "atlas22_shaft_contact_upper_stack_floor_plane",
|
||||
"reference_images": [
|
||||
"blowjob_top_view/22_blowjob_top_view.png"
|
||||
],
|
||||
"score": {
|
||||
"anatomy_proportion": "pass",
|
||||
"atlas_pose_match": "pass",
|
||||
"clothing_visibility": "pass",
|
||||
"contact_match": "pass",
|
||||
"expression_eye_control": "partial",
|
||||
"pose_ownership": "pass",
|
||||
"prompt_noise": "pass",
|
||||
"subject_identity": "pass",
|
||||
"workspace_continuity": "partial"
|
||||
},
|
||||
"seed": 238365845574312,
|
||||
"seed_metadata": {
|
||||
"atlas_cue_seed": null,
|
||||
"generator_seed": null,
|
||||
"micro_position_seed": null,
|
||||
"sampler_seed": 238365845574312,
|
||||
"workspace_seed": null
|
||||
},
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Straight-down male POV oral close-up. The centered large penis and mouth contact form the main vertical axis from the lower foreground to the woman's face. The woman kneels directly below the penis. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, upper chest, neckline, and one hand stack around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Tucked knees sit small on the floor beside the upper-body stack. Background reads as a flat pale floor plane with one cropped support edge and shallow overhead room depth.",
|
||||
"turn": 90,
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft"
|
||||
},
|
||||
{
|
||||
"analysis_notes": "Turn 91 is the best same-seed controlled candidate in this batch: shaft/contact axis holds, the woman remains between the viewer legs, and cropped desk/chair/carpet anchors preserve top-down coworking evidence without returning to deep lounge perspective.",
|
||||
"blockers": [],
|
||||
"cue_axes": {
|
||||
"anatomy_shape_detail": "head_and_shoulders_closer_than_knees",
|
||||
"body_angle": "directly_below_viewer_between_feet",
|
||||
"camera_height": "standing_nadir_overhead_shaft_first",
|
||||
"clothing_visibility": "upper_body_clothing_visible_from_above",
|
||||
"contact_depth": "shaft_contact_axis_centered_between_viewer_feet",
|
||||
"expression_eye_detail": "face_looking_up_from_below_camera",
|
||||
"foot_position": "viewer_feet_lower_side_anchors",
|
||||
"hand_position": "one_hand_base_contact",
|
||||
"workspace_surface": "floor_texture_with_cropped_furniture_edge_anchors"
|
||||
},
|
||||
"decision": "seedable_candidate",
|
||||
"id": "pov_blowjob_top_down_vertical_shaft_00001__atlas27_shaft_axis_between_feet_floor_anchors",
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_a2341f39a45e41ed96b93dcc5a89d372.png",
|
||||
"prompt_order": "subject_first",
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "atlas27_shaft_axis_between_feet_floor_anchors",
|
||||
"tested_text_sha256": "041e07abe9389df875267f5aa76d94bb9387f9d232ca4331e1e2efcadf2de497"
|
||||
},
|
||||
"prompt_variant_id": "atlas27_shaft_axis_between_feet_floor_anchors",
|
||||
"reference_images": [
|
||||
"blowjob_top_view/27_blowjob_top_view.png"
|
||||
],
|
||||
"score": {
|
||||
"anatomy_proportion": "pass",
|
||||
"atlas_pose_match": "pass",
|
||||
"clothing_visibility": "pass",
|
||||
"contact_match": "pass",
|
||||
"expression_eye_control": "partial",
|
||||
"pose_ownership": "pass",
|
||||
"prompt_noise": "pass",
|
||||
"subject_identity": "pass",
|
||||
"workspace_continuity": "pass"
|
||||
},
|
||||
"seed": 238365845574312,
|
||||
"seed_metadata": {
|
||||
"atlas_cue_seed": null,
|
||||
"generator_seed": null,
|
||||
"micro_position_seed": null,
|
||||
"sampler_seed": 238365845574312,
|
||||
"workspace_seed": null
|
||||
},
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing nadir male POV oral close-up. The centered large penis rises from the lower foreground and points directly to the woman's mouth between the viewer's feet. The woman kneels directly below the viewer between his feet. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, hands, and knees are visible from above around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Cropped desk legs, chair wheels, carpet texture, and floor seams sit near the frame edges as top-down workspace anchors.",
|
||||
"turn": 91,
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft"
|
||||
}
|
||||
],
|
||||
"promotion_ready_count": 1,
|
||||
"required_pass_keys": [
|
||||
"pose_ownership",
|
||||
"workspace_continuity",
|
||||
"clothing_visibility",
|
||||
"subject_identity",
|
||||
"prompt_noise"
|
||||
],
|
||||
"required_progress_keys": [
|
||||
"atlas_pose_match",
|
||||
"contact_match",
|
||||
"expression_eye_control",
|
||||
"anatomy_proportion"
|
||||
],
|
||||
"schema": "sxcp_atlas_refine_promotion_report_v1",
|
||||
"seed": 238365845574312,
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"subject_id": "atlas_refine_same_woman_001",
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft"
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"baseline_probe_id": "pov_blowjob_top_down_vertical_shaft_00001__baseline",
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"notes": "Same-seed shaft-first exact-text top-view oral calibration. Baseline turn 89 keeps deep coworking perspective; candidate turn 90 improves shaft-axis but loses workspace anchors; candidate turn 91 best preserves shaft-axis and top-down workspace edge anchors.",
|
||||
"probe_count": 3,
|
||||
"probes": [
|
||||
{
|
||||
"analysis_notes": "Baseline turn 89 keeps subject and contact, but deep coworking-room perspective and abdomen-first foreground make it read less like the atlas top-view family.",
|
||||
"cue_axes": {
|
||||
"anatomy_shape_detail": null,
|
||||
"body_angle": null,
|
||||
"camera_height": null,
|
||||
"clothing_visibility": null,
|
||||
"contact_depth": null,
|
||||
"expression_eye_detail": null,
|
||||
"foot_position": null,
|
||||
"hand_position": null,
|
||||
"workspace_surface": null
|
||||
},
|
||||
"id": "pov_blowjob_top_down_vertical_shaft_00001__baseline",
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_24af2d34756248cca358567f2a692891.png",
|
||||
"prompt_order": "subject_first",
|
||||
"prompt_source": {
|
||||
"kind": "baseline",
|
||||
"tested_text_sha256": "1d0b95d9865d1a502fb91bc856b3ff4baf00da90248c47d45631fb512f58a463"
|
||||
},
|
||||
"returned_seed": 238365845574312,
|
||||
"score": {
|
||||
"anatomy_proportion": null,
|
||||
"atlas_pose_match": null,
|
||||
"clothing_visibility": null,
|
||||
"contact_match": null,
|
||||
"expression_eye_control": null,
|
||||
"pose_ownership": null,
|
||||
"prompt_noise": null,
|
||||
"subject_identity": null,
|
||||
"workspace_continuity": null
|
||||
},
|
||||
"seed_metadata": {
|
||||
"atlas_cue_seed": null,
|
||||
"generator_seed": null,
|
||||
"micro_position_seed": null,
|
||||
"sampler_seed": 238365845574312,
|
||||
"workspace_seed": null
|
||||
},
|
||||
"selection": {},
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir-angle standing male POV top-view oral position. viewer looks almost straight down from his torso toward the floor. nearby carpet/floor plane dominates the image. viewer abdomen, shorts, thighs, and feet frame the lower foreground. large penis is a short centered vertical column. the woman kneels directly below the viewer between his feet. her mouth seals around the centered large penis. one hand wraps the base. hair crown, forehead, shoulders, hands, and knees are visible from above. desk legs, chair wheels, carpet texture, and floor seams act as top-down office anchors. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.",
|
||||
"turn": 89,
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft"
|
||||
},
|
||||
{
|
||||
"analysis_notes": "Turn 90 strongly improves shaft/contact verticality and upper-body stack over baseline. It loses most coworking workspace identity, so keep it as shaft-anchor evidence rather than seedable workspace-continuity evidence.",
|
||||
"cue_axes": {
|
||||
"anatomy_shape_detail": "small_tucked_knees_beside_upper_body_stack",
|
||||
"body_angle": "compact_kneeling_upper_body_stack_below_shaft",
|
||||
"camera_height": "straight_down_overhead_shaft_first",
|
||||
"clothing_visibility": "upper_body_neckline_anchor",
|
||||
"contact_depth": "shaft_contact_axis_centered_from_lower_foreground_to_mouth",
|
||||
"expression_eye_detail": "upward_face_or_lowered_eyelids_visible_from_above",
|
||||
"foot_position": null,
|
||||
"hand_position": "one_hand_base_contact",
|
||||
"workspace_surface": "flat_pale_floor_with_cropped_support_edge_shallow_depth"
|
||||
},
|
||||
"id": "pov_blowjob_top_down_vertical_shaft_00001__atlas22_shaft_contact_upper_stack_floor_plane",
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_cca7cd5c69894cbb899f94b172faa5d1.png",
|
||||
"prompt_order": "subject_first",
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "atlas22_shaft_contact_upper_stack_floor_plane",
|
||||
"tested_text_sha256": "49495cc80d3ea87a49b46b20aa716b4e07ebc3179617b6252c310bcde7788848"
|
||||
},
|
||||
"reference_images": [
|
||||
"blowjob_top_view/22_blowjob_top_view.png"
|
||||
],
|
||||
"returned_seed": 238365845574312,
|
||||
"score": {
|
||||
"pose_ownership": "pass",
|
||||
"workspace_continuity": "partial",
|
||||
"clothing_visibility": "pass",
|
||||
"subject_identity": "pass",
|
||||
"prompt_noise": "pass",
|
||||
"atlas_pose_match": "pass",
|
||||
"contact_match": "pass",
|
||||
"expression_eye_control": "partial",
|
||||
"anatomy_proportion": "pass"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"atlas_cue_seed": null,
|
||||
"generator_seed": null,
|
||||
"micro_position_seed": null,
|
||||
"sampler_seed": 238365845574312,
|
||||
"workspace_seed": null
|
||||
},
|
||||
"selection": {},
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Straight-down male POV oral close-up. The centered large penis and mouth contact form the main vertical axis from the lower foreground to the woman's face. The woman kneels directly below the penis. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, upper chest, neckline, and one hand stack around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Tucked knees sit small on the floor beside the upper-body stack. Background reads as a flat pale floor plane with one cropped support edge and shallow overhead room depth.",
|
||||
"turn": 90,
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft"
|
||||
},
|
||||
{
|
||||
"analysis_notes": "Turn 91 is the best same-seed controlled candidate in this batch: shaft/contact axis holds, the woman remains between the viewer legs, and cropped desk/chair/carpet anchors preserve top-down coworking evidence without returning to deep lounge perspective.",
|
||||
"cue_axes": {
|
||||
"anatomy_shape_detail": "head_and_shoulders_closer_than_knees",
|
||||
"body_angle": "directly_below_viewer_between_feet",
|
||||
"camera_height": "standing_nadir_overhead_shaft_first",
|
||||
"clothing_visibility": "upper_body_clothing_visible_from_above",
|
||||
"contact_depth": "shaft_contact_axis_centered_between_viewer_feet",
|
||||
"expression_eye_detail": "face_looking_up_from_below_camera",
|
||||
"foot_position": "viewer_feet_lower_side_anchors",
|
||||
"hand_position": "one_hand_base_contact",
|
||||
"workspace_surface": "floor_texture_with_cropped_furniture_edge_anchors"
|
||||
},
|
||||
"id": "pov_blowjob_top_down_vertical_shaft_00001__atlas27_shaft_axis_between_feet_floor_anchors",
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_a2341f39a45e41ed96b93dcc5a89d372.png",
|
||||
"prompt_order": "subject_first",
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "atlas27_shaft_axis_between_feet_floor_anchors",
|
||||
"tested_text_sha256": "041e07abe9389df875267f5aa76d94bb9387f9d232ca4331e1e2efcadf2de497"
|
||||
},
|
||||
"reference_images": [
|
||||
"blowjob_top_view/27_blowjob_top_view.png"
|
||||
],
|
||||
"returned_seed": 238365845574312,
|
||||
"score": {
|
||||
"pose_ownership": "pass",
|
||||
"workspace_continuity": "pass",
|
||||
"clothing_visibility": "pass",
|
||||
"subject_identity": "pass",
|
||||
"prompt_noise": "pass",
|
||||
"atlas_pose_match": "pass",
|
||||
"contact_match": "pass",
|
||||
"expression_eye_control": "partial",
|
||||
"anatomy_proportion": "pass"
|
||||
},
|
||||
"seed_metadata": {
|
||||
"atlas_cue_seed": null,
|
||||
"generator_seed": null,
|
||||
"micro_position_seed": null,
|
||||
"sampler_seed": 238365845574312,
|
||||
"workspace_seed": null
|
||||
},
|
||||
"selection": {},
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing nadir male POV oral close-up. The centered large penis rises from the lower foreground and points directly to the woman's mouth between the viewer's feet. The woman kneels directly below the viewer between his feet. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, hands, and knees are visible from above around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Cropped desk legs, chair wheels, carpet texture, and floor seams sit near the frame edges as top-down workspace anchors.",
|
||||
"turn": 91,
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft"
|
||||
}
|
||||
],
|
||||
"schema": "sxcp_atlas_refine_result_sheet_v1",
|
||||
"score_keys": [
|
||||
"atlas_pose_match",
|
||||
"contact_match",
|
||||
"pose_ownership",
|
||||
"workspace_continuity",
|
||||
"clothing_visibility",
|
||||
"subject_identity",
|
||||
"expression_eye_control",
|
||||
"anatomy_proportion",
|
||||
"prompt_noise"
|
||||
],
|
||||
"seed": 238365845574312,
|
||||
"selection": {},
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_prompt_sha256": "1d0b95d9865d1a502fb91bc856b3ff4baf00da90248c47d45631fb512f58a463",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"subject_id": "atlas_refine_same_woman_001",
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft"
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"ready_candidate_count": 1,
|
||||
"schema": "sxcp_atlas_refine_sidecar_update_draft_v1",
|
||||
"seed": 238365845574312,
|
||||
"skipped_candidate_count": 1,
|
||||
"subject_id": "atlas_refine_same_woman_001",
|
||||
"update_count": 1,
|
||||
"updates": [
|
||||
{
|
||||
"prompt_variants": [
|
||||
{
|
||||
"cue_axes": {
|
||||
"anatomy_shape_detail": "head_and_shoulders_closer_than_knees",
|
||||
"body_angle": "directly_below_viewer_between_feet",
|
||||
"camera_height": "standing_nadir_overhead_shaft_first",
|
||||
"clothing_visibility": "upper_body_clothing_visible_from_above",
|
||||
"contact_depth": "shaft_contact_axis_centered_between_viewer_feet",
|
||||
"expression_eye_detail": "face_looking_up_from_below_camera",
|
||||
"foot_position": "viewer_feet_lower_side_anchors",
|
||||
"hand_position": "one_hand_base_contact",
|
||||
"workspace_surface": "floor_texture_with_cropped_furniture_edge_anchors"
|
||||
},
|
||||
"evidence": {
|
||||
"image_path": "/media/unraid/comfyui/output/agent_bridge/img_a2341f39a45e41ed96b93dcc5a89d372.png",
|
||||
"reference_images": [
|
||||
"blowjob_top_view/27_blowjob_top_view.png"
|
||||
],
|
||||
"score": {
|
||||
"anatomy_proportion": "pass",
|
||||
"atlas_pose_match": "pass",
|
||||
"clothing_visibility": "pass",
|
||||
"contact_match": "pass",
|
||||
"expression_eye_control": "partial",
|
||||
"pose_ownership": "pass",
|
||||
"prompt_noise": "pass",
|
||||
"subject_identity": "pass",
|
||||
"workspace_continuity": "pass"
|
||||
},
|
||||
"seed": 238365845574312,
|
||||
"turn": 91
|
||||
},
|
||||
"id": "atlas27_shaft_axis_between_feet_floor_anchors",
|
||||
"notes": "Turn 91 is the best same-seed controlled candidate in this batch: shaft/contact axis holds, the woman remains between the viewer legs, and cropped desk/chair/carpet anchors preserve top-down coworking evidence without returning to deep lounge perspective.",
|
||||
"prompt_order": "subject_first",
|
||||
"prompt_source": {
|
||||
"kind": "text",
|
||||
"prompt_variant_id": "atlas27_shaft_axis_between_feet_floor_anchors",
|
||||
"tested_text_sha256": "041e07abe9389df875267f5aa76d94bb9387f9d232ca4331e1e2efcadf2de497"
|
||||
},
|
||||
"reference_images": [
|
||||
"blowjob_top_view/27_blowjob_top_view.png"
|
||||
],
|
||||
"seed_metadata": {
|
||||
"atlas_cue_seed": null,
|
||||
"generator_seed": null,
|
||||
"micro_position_seed": null,
|
||||
"sampler_seed": 238365845574312,
|
||||
"workspace_seed": null
|
||||
},
|
||||
"text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing nadir male POV oral close-up. The centered large penis rises from the lower foreground and points directly to the woman's mouth between the viewer's feet. The woman kneels directly below the viewer between his feet. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, hands, and knees are visible from above around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Cropped desk legs, chair wheels, carpet texture, and floor seams sit near the frame edges as top-down workspace anchors."
|
||||
}
|
||||
],
|
||||
"sidecar_filename": "pov_blowjob_top_down_vertical_shaft_00001_.json",
|
||||
"source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001",
|
||||
"source_stem": "pov_blowjob_top_down_vertical_shaft_00001_",
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft"
|
||||
}
|
||||
],
|
||||
"variant_key": "pov_blowjob_top_down_vertical_shaft"
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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 = ""
|
||||
style_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 "",
|
||||
"style_config": request.style_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,295 @@
|
||||
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
|
||||
style_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)
|
||||
clothing_rng = deps.axis_rng(parsed_seed_config, "clothing", 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(clothing_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,
|
||||
seed_config=parsed_seed_config,
|
||||
)
|
||||
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,
|
||||
seed_config=parsed_seed_config,
|
||||
)
|
||||
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,
|
||||
request.style_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,633 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
|
||||
CAMERA_ORBIT_FRAMING_CHOICES = [
|
||||
"from_zoom",
|
||||
"wide",
|
||||
"medium",
|
||||
"full_body",
|
||||
"three_quarter",
|
||||
"close_up",
|
||||
"extreme_close_up",
|
||||
]
|
||||
CAMERA_ORBIT_FOCUS_CHOICES = [
|
||||
"auto",
|
||||
"face",
|
||||
"torso",
|
||||
"hips",
|
||||
"full_body",
|
||||
"action",
|
||||
"contact_points",
|
||||
"environment",
|
||||
]
|
||||
|
||||
CAMERA_MODE_PROMPTS = {
|
||||
"disabled": "",
|
||||
"standard": "",
|
||||
"handheld_selfie": (
|
||||
"Camera mode: handheld smartphone selfie, close arm-length framing, visible creator-shot perspective, "
|
||||
"slight wide-angle intimacy, direct eye contact, natural phone-camera composition."
|
||||
),
|
||||
"mirror_selfie": (
|
||||
"Camera mode: mirror selfie with the phone visible in one hand, reflective framing, creator looking at the screen, "
|
||||
"body and environment visible through the mirror."
|
||||
),
|
||||
"phone_tripod": (
|
||||
"Camera mode: phone on tripod or ring-light stand, creator-facing social-video framing, stable vertical composition, "
|
||||
"hands-free self-recorded setup."
|
||||
),
|
||||
"creator_pov": (
|
||||
"Camera mode: creator-held POV, intimate subscriber-view angle, the creator controls the camera, close foreground body framing."
|
||||
),
|
||||
"bed_selfie": (
|
||||
"Camera mode: bed selfie shot from a phone held above or beside the body, intimate close framing, sheets visible around the subject."
|
||||
),
|
||||
"bathroom_mirror": (
|
||||
"Camera mode: bathroom mirror selfie, phone visible, tiled private room, close vertical framing, candid creator-shot energy."
|
||||
),
|
||||
"phone_flash": (
|
||||
"Camera mode: direct phone-flash selfie, crisp flash highlights, candid night-post feeling, hard-edged smartphone shadows."
|
||||
),
|
||||
"action_cam": (
|
||||
"Camera mode: body-mounted or handheld action-camera intimacy, very close wide-angle perspective, dynamic creator-shot framing."
|
||||
),
|
||||
}
|
||||
|
||||
CAMERA_COMPACT_LABELS = {
|
||||
"disabled": "",
|
||||
"standard": "",
|
||||
"handheld_selfie": "handheld smartphone selfie",
|
||||
"mirror_selfie": "mirror selfie",
|
||||
"phone_tripod": "phone tripod / ring-light setup",
|
||||
"creator_pov": "creator-held POV",
|
||||
"bed_selfie": "bed selfie",
|
||||
"bathroom_mirror": "bathroom mirror selfie",
|
||||
"phone_flash": "phone-flash selfie",
|
||||
"action_cam": "handheld action-camera view",
|
||||
"full_body": "full body",
|
||||
"three_quarter": "three-quarter body",
|
||||
"waist_up": "waist-up",
|
||||
"close_up": "close-up",
|
||||
"extreme_close_up": "extreme close-up",
|
||||
"eye_level": "eye-level",
|
||||
"high_angle": "high-angle",
|
||||
"low_angle": "low-angle",
|
||||
"overhead": "overhead",
|
||||
"side_profile": "side-profile",
|
||||
"rear_view": "rear-view",
|
||||
"mirror_reflection": "mirror reflection",
|
||||
"smartphone_wide": "smartphone wide-angle",
|
||||
"ultra_wide": "ultra-wide",
|
||||
"portrait_lens": "phone portrait lens",
|
||||
"telephoto": "telephoto-style",
|
||||
"macro_detail": "macro detail",
|
||||
"arm_length": "arm-length",
|
||||
"near_body": "near-body",
|
||||
"bedside": "bedside phone",
|
||||
"room_corner": "room-corner phone",
|
||||
"vertical_story": "vertical 9:16",
|
||||
"square_feed": "square feed",
|
||||
"horizontal": "horizontal",
|
||||
"phone_visible": "phone visible",
|
||||
"phone_hidden": "phone hidden",
|
||||
"screen_reflection": "screen reflection",
|
||||
"ring_light_visible": "ring light visible",
|
||||
}
|
||||
|
||||
CAMERA_SHOT_PROMPTS = {
|
||||
"auto": "",
|
||||
"full_body": "Shot size: full body visible, head-to-toe framing, no important body parts cropped out.",
|
||||
"three_quarter": "Shot size: three-quarter body framing, face, torso, hips, and thighs clearly visible.",
|
||||
"waist_up": "Shot size: waist-up creator framing with face and upper body as the focus.",
|
||||
"close_up": "Shot size: close-up framing with face, expression, hands, and body contact emphasized.",
|
||||
"extreme_close_up": "Shot size: extreme close-up detail shot, tightly framed and intimate.",
|
||||
}
|
||||
|
||||
CAMERA_ANGLE_PROMPTS = {
|
||||
"auto": "",
|
||||
"eye_level": "Angle: eye-level camera angle with direct creator eye contact.",
|
||||
"high_angle": "Angle: high-angle selfie looking down toward the body.",
|
||||
"low_angle": "Angle: low-angle phone camera looking upward from near the body.",
|
||||
"overhead": "Angle: overhead phone shot looking down at the full pose.",
|
||||
"side_profile": "Angle: side-profile camera view emphasizing body silhouette and contact points.",
|
||||
"rear_view": "Angle: rear-view camera framing with the body turned away from the lens.",
|
||||
"mirror_reflection": "Angle: mirror-reflection composition with the phone and reflected body placement readable.",
|
||||
}
|
||||
|
||||
CAMERA_LENS_PROMPTS = {
|
||||
"auto": "",
|
||||
"smartphone_wide": "Lens: smartphone wide-angle lens with slight edge distortion and close personal scale.",
|
||||
"ultra_wide": "Lens: ultra-wide phone lens, exaggerated near-camera perspective, environmental context visible.",
|
||||
"portrait_lens": "Lens: phone portrait mode, shallow depth of field, crisp subject separation.",
|
||||
"telephoto": "Lens: compressed telephoto-style framing, flatter proportions, less distortion.",
|
||||
"macro_detail": "Lens: macro-detail phone shot focused on texture, skin, fabric, and contact detail.",
|
||||
}
|
||||
|
||||
CAMERA_DISTANCE_PROMPTS = {
|
||||
"auto": "",
|
||||
"arm_length": "Camera distance: arm-length selfie distance, close enough to feel handheld.",
|
||||
"near_body": "Camera distance: near-body camera placement with intimate foreground framing.",
|
||||
"bedside": "Camera distance: phone placed beside the body on the bed or floor.",
|
||||
"room_corner": "Camera distance: phone set across the room, self-recorded but wider and more observational.",
|
||||
}
|
||||
|
||||
CAMERA_ORIENTATION_PROMPTS = {
|
||||
"auto": "",
|
||||
"vertical_story": "Orientation: vertical 9:16 story/reel framing.",
|
||||
"square_feed": "Orientation: square social-feed crop.",
|
||||
"horizontal": "Orientation: horizontal phone-video crop.",
|
||||
}
|
||||
|
||||
CAMERA_PHONE_PROMPTS = {
|
||||
"auto": "",
|
||||
"phone_visible": "Phone visibility: phone visible in hand or mirror, clearly creator-shot.",
|
||||
"phone_hidden": "Phone visibility: phone is implied but not visible, preserving the selfie/creator-shot perspective.",
|
||||
"screen_reflection": "Phone visibility: screen glow or reflection visible in the scene.",
|
||||
"ring_light_visible": "Phone visibility: ring light or tripod visible enough to read as self-recorded content.",
|
||||
}
|
||||
|
||||
CAMERA_PRIORITY_PROMPTS = {
|
||||
"soft_hint": "Camera priority: treat the camera notes as style guidance.",
|
||||
"strong": "Camera priority: strongly preserve the selected camera, lens, angle, crop, and phone-shot perspective.",
|
||||
"locked": "Camera priority: locked camera constraint; do not replace this with a studio, third-person, cinematic, or unrelated camera view.",
|
||||
}
|
||||
|
||||
QWEN_CAMERA_DIRECTIONS = {
|
||||
"front-right quarter view": 45,
|
||||
"right side view": 90,
|
||||
"back-right quarter view": 135,
|
||||
"back view": 180,
|
||||
"back-left quarter view": 225,
|
||||
"left side view": 270,
|
||||
"front-left quarter view": 315,
|
||||
"front view": 0,
|
||||
}
|
||||
QWEN_CAMERA_ELEVATIONS = {
|
||||
"low-angle shot": -30,
|
||||
"eye-level shot": 0,
|
||||
"elevated shot": 30,
|
||||
"high-angle shot": 60,
|
||||
}
|
||||
QWEN_CAMERA_ZOOMS = {
|
||||
"wide shot": 0.0,
|
||||
"medium shot": 5.0,
|
||||
"close-up": 8.0,
|
||||
}
|
||||
QWEN_CAMERA_SCENE_CENTER_Y = 0.5
|
||||
|
||||
|
||||
def _is_false(value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value is False
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ("false", "0", "no", "off")
|
||||
return False
|
||||
|
||||
|
||||
def _choice(value: Any, choices: dict[str, str], default: str) -> str:
|
||||
value = str(value or default)
|
||||
return value if value in choices else default
|
||||
|
||||
|
||||
def _clean_prompt_punctuation(text: str) -> str:
|
||||
text = re.sub(r"\s+", " ", str(text or "")).strip()
|
||||
text = re.sub(r"\s+([,.;:])", r"\1", text)
|
||||
text = re.sub(r"(?:,\s*){2,}", ", ", text)
|
||||
text = re.sub(r"\.\s*\.", ".", text)
|
||||
text = re.sub(r":\s*\.", ".", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def camera_mode_choices() -> list[str]:
|
||||
return list(CAMERA_MODE_PROMPTS)
|
||||
|
||||
|
||||
def camera_detail_choices() -> list[str]:
|
||||
return list(CAMERA_DETAIL_CHOICES)
|
||||
|
||||
|
||||
def camera_orbit_framing_choices() -> list[str]:
|
||||
return list(CAMERA_ORBIT_FRAMING_CHOICES)
|
||||
|
||||
|
||||
def camera_orbit_focus_choices() -> list[str]:
|
||||
return list(CAMERA_ORBIT_FOCUS_CHOICES)
|
||||
|
||||
|
||||
def camera_shot_choices() -> list[str]:
|
||||
return list(CAMERA_SHOT_PROMPTS)
|
||||
|
||||
|
||||
def camera_angle_choices() -> list[str]:
|
||||
return list(CAMERA_ANGLE_PROMPTS)
|
||||
|
||||
|
||||
def camera_lens_choices() -> list[str]:
|
||||
return list(CAMERA_LENS_PROMPTS)
|
||||
|
||||
|
||||
def camera_distance_choices() -> list[str]:
|
||||
return list(CAMERA_DISTANCE_PROMPTS)
|
||||
|
||||
|
||||
def camera_orientation_choices() -> list[str]:
|
||||
return list(CAMERA_ORIENTATION_PROMPTS)
|
||||
|
||||
|
||||
def camera_phone_choices() -> list[str]:
|
||||
return list(CAMERA_PHONE_PROMPTS)
|
||||
|
||||
|
||||
def camera_priority_choices() -> list[str]:
|
||||
return list(CAMERA_PRIORITY_PROMPTS)
|
||||
|
||||
|
||||
def build_camera_config_json(
|
||||
camera_mode: str = "standard",
|
||||
shot_size: str = "auto",
|
||||
angle: str = "auto",
|
||||
lens: str = "auto",
|
||||
distance: str = "auto",
|
||||
orientation: str = "auto",
|
||||
phone_visibility: str = "auto",
|
||||
priority: str = "strong",
|
||||
camera_detail: str = "compact",
|
||||
) -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"camera_mode": camera_mode,
|
||||
"shot_size": shot_size,
|
||||
"angle": angle,
|
||||
"lens": lens,
|
||||
"distance": distance,
|
||||
"orientation": orientation,
|
||||
"phone_visibility": phone_visibility,
|
||||
"priority": priority,
|
||||
"camera_detail": camera_detail,
|
||||
},
|
||||
ensure_ascii=True,
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
|
||||
def _camera_orbit_direction(horizontal_angle: Any) -> str:
|
||||
h_angle = int(float(horizontal_angle or 0)) % 360
|
||||
if h_angle < 22.5 or h_angle >= 337.5:
|
||||
return "front view"
|
||||
if h_angle < 67.5:
|
||||
return "front-right quarter view"
|
||||
if h_angle < 112.5:
|
||||
return "right side view"
|
||||
if h_angle < 157.5:
|
||||
return "back-right quarter view"
|
||||
if h_angle < 202.5:
|
||||
return "back view"
|
||||
if h_angle < 247.5:
|
||||
return "back-left quarter view"
|
||||
if h_angle < 292.5:
|
||||
return "left side view"
|
||||
return "front-left quarter view"
|
||||
|
||||
|
||||
def _camera_orbit_elevation(vertical_angle: Any) -> str:
|
||||
vertical = int(float(vertical_angle or 0))
|
||||
if vertical < -15:
|
||||
return "low-angle shot"
|
||||
if vertical < 15:
|
||||
return "eye-level shot"
|
||||
if vertical < 45:
|
||||
return "elevated shot"
|
||||
return "high-angle shot"
|
||||
|
||||
|
||||
def _camera_orbit_distance(zoom: Any, framing: str = "from_zoom") -> str:
|
||||
framing = framing if framing in CAMERA_ORBIT_FRAMING_CHOICES else "from_zoom"
|
||||
framing_labels = {
|
||||
"wide": "wide shot",
|
||||
"medium": "medium shot",
|
||||
"full_body": "full-body shot",
|
||||
"three_quarter": "three-quarter body shot",
|
||||
"close_up": "close-up",
|
||||
"extreme_close_up": "extreme close-up",
|
||||
}
|
||||
if framing != "from_zoom":
|
||||
return framing_labels[framing]
|
||||
zoom_value = float(zoom or 0.0)
|
||||
if zoom_value < 2:
|
||||
return "wide shot"
|
||||
if zoom_value < 6:
|
||||
return "medium shot"
|
||||
return "close-up"
|
||||
|
||||
|
||||
def _camera_orbit_focus(subject_focus: str) -> str:
|
||||
return {
|
||||
"face": "face and expression centered",
|
||||
"torso": "torso and hands centered",
|
||||
"hips": "hips and lower body centered",
|
||||
"full_body": "full body centered",
|
||||
"action": "main action centered",
|
||||
"contact_points": "body contact points centered",
|
||||
"environment": "subject and room both readable",
|
||||
}.get(str(subject_focus or "auto"), "")
|
||||
|
||||
|
||||
def camera_orbit_prompt(
|
||||
horizontal_angle: Any,
|
||||
vertical_angle: Any,
|
||||
zoom: Any,
|
||||
framing: str = "from_zoom",
|
||||
subject_focus: str = "auto",
|
||||
include_degrees: bool = True,
|
||||
) -> tuple[str, dict[str, Any]]:
|
||||
azimuth = max(0, min(359, int(float(horizontal_angle or 0))))
|
||||
elevation = max(-90, min(90, int(float(vertical_angle or 0))))
|
||||
zoom_value = max(0.0, min(10.0, float(zoom or 0.0)))
|
||||
direction = _camera_orbit_direction(azimuth)
|
||||
elevation_label = _camera_orbit_elevation(elevation)
|
||||
distance_label = _camera_orbit_distance(zoom_value, framing)
|
||||
focus_label = _camera_orbit_focus(subject_focus)
|
||||
pieces = [direction, elevation_label, distance_label, focus_label]
|
||||
prompt = ", ".join(piece for piece in pieces if piece)
|
||||
if include_degrees:
|
||||
prompt = f"{azimuth}-degree {prompt}"
|
||||
return prompt, {
|
||||
"orbit_azimuth": azimuth,
|
||||
"orbit_elevation": elevation,
|
||||
"orbit_zoom": zoom_value,
|
||||
"orbit_direction": direction,
|
||||
"orbit_elevation_label": elevation_label,
|
||||
"orbit_distance_label": distance_label,
|
||||
"orbit_framing": framing if framing in CAMERA_ORBIT_FRAMING_CHOICES else "from_zoom",
|
||||
"orbit_focus": subject_focus if subject_focus in CAMERA_ORBIT_FOCUS_CHOICES else "auto",
|
||||
}
|
||||
|
||||
|
||||
def build_camera_orbit_config_json(
|
||||
enabled: bool = True,
|
||||
camera_mode: str = "standard",
|
||||
horizontal_angle: int = 0,
|
||||
vertical_angle: int = 0,
|
||||
zoom: float = 5.0,
|
||||
framing: str = "from_zoom",
|
||||
subject_focus: str = "auto",
|
||||
lens: str = "auto",
|
||||
orientation: str = "auto",
|
||||
phone_visibility: str = "auto",
|
||||
priority: str = "locked",
|
||||
camera_detail: str = "compact",
|
||||
include_degrees: bool = True,
|
||||
) -> str:
|
||||
orbit_prompt, orbit_metadata = camera_orbit_prompt(
|
||||
horizontal_angle,
|
||||
vertical_angle,
|
||||
zoom,
|
||||
framing=framing,
|
||||
subject_focus=subject_focus,
|
||||
include_degrees=include_degrees,
|
||||
)
|
||||
config = {
|
||||
"camera_mode": "disabled" if _is_false(enabled) else _choice(camera_mode, CAMERA_MODE_PROMPTS, "standard"),
|
||||
"shot_size": "auto",
|
||||
"angle": "auto",
|
||||
"lens": _choice(lens, CAMERA_LENS_PROMPTS, "auto"),
|
||||
"distance": "auto",
|
||||
"orientation": _choice(orientation, CAMERA_ORIENTATION_PROMPTS, "auto"),
|
||||
"phone_visibility": _choice(phone_visibility, CAMERA_PHONE_PROMPTS, "auto"),
|
||||
"priority": _choice(priority, CAMERA_PRIORITY_PROMPTS, "locked"),
|
||||
"camera_detail": camera_detail if camera_detail in CAMERA_DETAIL_CHOICES else "compact",
|
||||
"camera_source": "orbit",
|
||||
"custom_camera_prompt": orbit_prompt if not _is_false(enabled) else "",
|
||||
**orbit_metadata,
|
||||
}
|
||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def _qwen_prompt_camera_values(qwen_prompt: Any) -> tuple[int, int, float]:
|
||||
text = _clean_prompt_punctuation(str(qwen_prompt or "").lower().replace(",", " "))
|
||||
horizontal_angle = 0
|
||||
vertical_angle = 0
|
||||
zoom = 5.0
|
||||
for label, value in QWEN_CAMERA_DIRECTIONS.items():
|
||||
if label in text:
|
||||
horizontal_angle = value
|
||||
break
|
||||
for label, value in QWEN_CAMERA_ELEVATIONS.items():
|
||||
if label in text:
|
||||
vertical_angle = value
|
||||
break
|
||||
for label, value in QWEN_CAMERA_ZOOMS.items():
|
||||
if label in text:
|
||||
zoom = value
|
||||
break
|
||||
return horizontal_angle, vertical_angle, zoom
|
||||
|
||||
|
||||
def _camera_info_dict(camera_info: Any) -> dict[str, Any] | None:
|
||||
if not camera_info:
|
||||
return None
|
||||
if isinstance(camera_info, dict):
|
||||
return camera_info
|
||||
if isinstance(camera_info, str):
|
||||
try:
|
||||
raw = json.loads(camera_info)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return raw if isinstance(raw, dict) else None
|
||||
return None
|
||||
|
||||
|
||||
def _qwen_camera_info_values(camera_info: Any) -> tuple[int, int, float] | None:
|
||||
info = _camera_info_dict(camera_info)
|
||||
if not info:
|
||||
return None
|
||||
position = info.get("position") if isinstance(info.get("position"), dict) else {}
|
||||
target = info.get("target") if isinstance(info.get("target"), dict) else {}
|
||||
try:
|
||||
dx = float(position.get("x", 0.0)) - float(target.get("x", 0.0))
|
||||
dy = float(position.get("y", QWEN_CAMERA_SCENE_CENTER_Y)) - float(
|
||||
target.get("y", QWEN_CAMERA_SCENE_CENTER_Y)
|
||||
)
|
||||
dz = float(position.get("z", 0.0)) - float(target.get("z", 0.0))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
distance = math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||
if distance <= 0:
|
||||
return None
|
||||
horizontal_angle = int(round(math.degrees(math.atan2(dx, dz)))) % 360
|
||||
vertical_angle = int(round(math.degrees(math.asin(max(-1.0, min(1.0, dy / distance))))))
|
||||
zoom = max(0.0, min(10.0, ((2.6 - distance) / 2.0) * 10.0))
|
||||
return horizontal_angle, vertical_angle, round(zoom, 2)
|
||||
|
||||
|
||||
def build_qwen_camera_config_json(
|
||||
qwen_prompt: str = "",
|
||||
camera_info: Any = None,
|
||||
prefer_camera_info: bool = True,
|
||||
camera_mode: str = "standard",
|
||||
subject_focus: str = "auto",
|
||||
lens: str = "auto",
|
||||
orientation: str = "auto",
|
||||
phone_visibility: str = "auto",
|
||||
priority: str = "locked",
|
||||
camera_detail: str = "compact",
|
||||
include_degrees: bool = False,
|
||||
suppress_phone_visibility: bool = True,
|
||||
) -> str:
|
||||
info_values = _qwen_camera_info_values(camera_info)
|
||||
if prefer_camera_info and info_values is not None:
|
||||
horizontal_angle, vertical_angle, zoom = info_values
|
||||
source = "qwen_multiangle_camera_info"
|
||||
else:
|
||||
horizontal_angle, vertical_angle, zoom = _qwen_prompt_camera_values(qwen_prompt)
|
||||
source = "qwen_multiangle_prompt"
|
||||
config = json.loads(
|
||||
build_camera_orbit_config_json(
|
||||
enabled=True,
|
||||
camera_mode=camera_mode,
|
||||
horizontal_angle=horizontal_angle,
|
||||
vertical_angle=vertical_angle,
|
||||
zoom=zoom,
|
||||
framing="from_zoom",
|
||||
subject_focus=subject_focus,
|
||||
lens=lens,
|
||||
orientation=orientation,
|
||||
phone_visibility="auto" if not _is_false(suppress_phone_visibility) else phone_visibility,
|
||||
priority=priority,
|
||||
camera_detail=camera_detail,
|
||||
include_degrees=include_degrees,
|
||||
)
|
||||
)
|
||||
config["camera_source"] = source
|
||||
config["qwen_prompt"] = str(qwen_prompt or "").strip()
|
||||
if info_values is not None:
|
||||
config["qwen_camera_info_values"] = {
|
||||
"horizontal_angle": info_values[0],
|
||||
"vertical_angle": info_values[1],
|
||||
"zoom": info_values[2],
|
||||
}
|
||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def parse_camera_config(camera_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
defaults = {
|
||||
"camera_mode": "standard",
|
||||
"shot_size": "auto",
|
||||
"angle": "auto",
|
||||
"lens": "auto",
|
||||
"distance": "auto",
|
||||
"orientation": "auto",
|
||||
"phone_visibility": "auto",
|
||||
"priority": "strong",
|
||||
"camera_detail": "compact",
|
||||
}
|
||||
if not camera_config:
|
||||
return defaults
|
||||
if isinstance(camera_config, dict):
|
||||
raw = camera_config
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(camera_config))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid camera_config JSON: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("camera_config must be a JSON object")
|
||||
parsed = {**defaults, **raw}
|
||||
custom_camera_prompt = _clean_prompt_punctuation(parsed.get("custom_camera_prompt", "")).rstrip(".")
|
||||
camera_source = str(parsed.get("camera_source") or "")
|
||||
normalized = {
|
||||
"camera_mode": _choice(parsed.get("camera_mode"), CAMERA_MODE_PROMPTS, defaults["camera_mode"]),
|
||||
"shot_size": _choice(parsed.get("shot_size"), CAMERA_SHOT_PROMPTS, defaults["shot_size"]),
|
||||
"angle": _choice(parsed.get("angle"), CAMERA_ANGLE_PROMPTS, defaults["angle"]),
|
||||
"lens": _choice(parsed.get("lens"), CAMERA_LENS_PROMPTS, defaults["lens"]),
|
||||
"distance": _choice(parsed.get("distance"), CAMERA_DISTANCE_PROMPTS, defaults["distance"]),
|
||||
"orientation": _choice(parsed.get("orientation"), CAMERA_ORIENTATION_PROMPTS, defaults["orientation"]),
|
||||
"phone_visibility": _choice(parsed.get("phone_visibility"), CAMERA_PHONE_PROMPTS, defaults["phone_visibility"]),
|
||||
"priority": _choice(parsed.get("priority"), CAMERA_PRIORITY_PROMPTS, defaults["priority"]),
|
||||
"camera_detail": str(parsed.get("camera_detail") or defaults["camera_detail"])
|
||||
if str(parsed.get("camera_detail") or defaults["camera_detail"]) in CAMERA_DETAIL_CHOICES
|
||||
else defaults["camera_detail"],
|
||||
}
|
||||
if custom_camera_prompt:
|
||||
normalized["custom_camera_prompt"] = custom_camera_prompt
|
||||
if camera_source:
|
||||
normalized["camera_source"] = camera_source
|
||||
for key in (
|
||||
"orbit_azimuth",
|
||||
"orbit_elevation",
|
||||
"orbit_zoom",
|
||||
"orbit_direction",
|
||||
"orbit_elevation_label",
|
||||
"orbit_distance_label",
|
||||
"orbit_framing",
|
||||
"orbit_focus",
|
||||
):
|
||||
if key in parsed:
|
||||
normalized[key] = parsed[key]
|
||||
return normalized
|
||||
|
||||
|
||||
def camera_config_with_mode(camera_config: str | dict[str, Any] | None, camera_mode: str) -> dict[str, Any]:
|
||||
parsed = parse_camera_config(camera_config)
|
||||
if camera_mode and camera_mode != "from_camera_config":
|
||||
parsed["camera_mode"] = _choice(camera_mode, CAMERA_MODE_PROMPTS, parsed["camera_mode"])
|
||||
return parsed
|
||||
|
||||
|
||||
def camera_directive(camera_config: str | dict[str, Any] | None) -> tuple[str, dict[str, Any]]:
|
||||
parsed = parse_camera_config(camera_config)
|
||||
if parsed["camera_detail"] == "off" or parsed["camera_mode"] == "disabled":
|
||||
return "", parsed
|
||||
custom_camera_prompt = str(parsed.get("custom_camera_prompt") or "").strip()
|
||||
if parsed["camera_detail"] == "compact":
|
||||
values = [
|
||||
parsed["camera_mode"],
|
||||
parsed["shot_size"],
|
||||
parsed["angle"],
|
||||
parsed["lens"],
|
||||
parsed["distance"],
|
||||
parsed["orientation"],
|
||||
parsed["phone_visibility"],
|
||||
]
|
||||
labels = [CAMERA_COMPACT_LABELS.get(value, value.replace("_", " ")) for value in values]
|
||||
labels = [label for value, label in zip(values, labels) if label and value != "auto"]
|
||||
if custom_camera_prompt:
|
||||
labels.append(custom_camera_prompt)
|
||||
if not labels:
|
||||
return "", parsed
|
||||
directive = "Camera: " + ", ".join(labels) + "."
|
||||
if parsed["priority"] == "locked":
|
||||
directive += " Keep this camera framing."
|
||||
return directive, parsed
|
||||
parts = [
|
||||
CAMERA_MODE_PROMPTS[parsed["camera_mode"]],
|
||||
CAMERA_SHOT_PROMPTS[parsed["shot_size"]],
|
||||
CAMERA_ANGLE_PROMPTS[parsed["angle"]],
|
||||
CAMERA_LENS_PROMPTS[parsed["lens"]],
|
||||
CAMERA_DISTANCE_PROMPTS[parsed["distance"]],
|
||||
CAMERA_ORIENTATION_PROMPTS[parsed["orientation"]],
|
||||
CAMERA_PHONE_PROMPTS[parsed["phone_visibility"]],
|
||||
]
|
||||
if custom_camera_prompt:
|
||||
parts.append(f"Camera orbit: {custom_camera_prompt}.")
|
||||
parts = [part for part in parts if part]
|
||||
if not parts:
|
||||
return "", parsed
|
||||
parts.append(CAMERA_PRIORITY_PROMPTS[parsed["priority"]])
|
||||
return " ".join(parts), parsed
|
||||
|
||||
|
||||
def camera_caption_text(parsed: dict[str, Any]) -> str:
|
||||
custom_camera_prompt = str(parsed.get("custom_camera_prompt") or "").strip()
|
||||
if custom_camera_prompt:
|
||||
return custom_camera_prompt
|
||||
camera_mode = str(parsed.get("camera_mode") or "").replace("_", " ").strip()
|
||||
if not camera_mode or camera_mode == "standard":
|
||||
return ""
|
||||
return f"{camera_mode} camera framing"
|
||||
@@ -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()
|
||||
@@ -0,0 +1,469 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
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)
|
||||
class CaptionMetadataRouteRequest:
|
||||
row: dict[str, Any]
|
||||
detail_level: str
|
||||
keep_style: bool
|
||||
target: str = "auto"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CaptionMetadataRoute:
|
||||
prose: str
|
||||
method: str
|
||||
|
||||
def as_tuple(self) -> tuple[str, str]:
|
||||
return self.prose, self.method
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CaptionMetadataRouteDependencies:
|
||||
item_labels: tuple[str, ...]
|
||||
clean_text: Callable[[Any], str]
|
||||
row_value: Callable[[dict[str, Any], str, tuple[str, ...]], str]
|
||||
field_row_value: Callable[[dict[str, Any], str], str]
|
||||
clean_clothing: Callable[[str], str]
|
||||
normalize_composition: Callable[[str], str]
|
||||
expression_disabled: Callable[[dict[str, Any]], bool]
|
||||
detail_allows: Callable[..., bool]
|
||||
join_sentences: Callable[[list[str]], str]
|
||||
human_join: Callable[[list[str]], str]
|
||||
article: Callable[[str], str]
|
||||
cap_first: Callable[[str], str]
|
||||
body_phrase: Callable[[Any, Any], str]
|
||||
single_caption_front: Callable[[dict[str, Any]], dict[str, str]]
|
||||
pose_clause: Callable[[str], str]
|
||||
age_subject: Callable[[str, str], str]
|
||||
clean_age_phrase: Callable[[str], str]
|
||||
subject_phrase_from_counts: Callable[[dict[str, Any]], str]
|
||||
verb_for_row: 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]
|
||||
cast_labels: Callable[[str], list[str]]
|
||||
natural_label_text: Callable[[Any, list[str]], str]
|
||||
softcore_caption_setup_phrase: Callable[..., str]
|
||||
metadata_to_prose: Callable[..., tuple[str, str]]
|
||||
|
||||
|
||||
def pronoun(subject: str) -> str:
|
||||
return "She" if subject == "woman" else "He"
|
||||
|
||||
|
||||
def possessive_pronoun(subject: str) -> str:
|
||||
return "Her" if subject == "woman" else "His"
|
||||
|
||||
|
||||
def couple_clothing_sentence(clothing: str, clean_text: Callable[[Any], str]) -> str:
|
||||
clothing = clean_text(clothing)
|
||||
lower = clothing.lower()
|
||||
partner_text = re.sub(r"\bPartner ([AB]) wears\b", r"Partner \1 wearing", clothing)
|
||||
partner_text = re.sub(r"\bPartner ([AB]) has\b", r"Partner \1 with", partner_text)
|
||||
if lower.startswith("partner a "):
|
||||
return f"The outfits show {partner_text}"
|
||||
if lower.startswith(("two ", "paired ", "coordinated ")):
|
||||
return f"The outfits are {partner_text}"
|
||||
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(
|
||||
request: CaptionMetadataRouteRequest,
|
||||
deps: CaptionMetadataRouteDependencies,
|
||||
) -> CaptionMetadataRoute | None:
|
||||
row = request.row
|
||||
detail_level = request.detail_level
|
||||
keep_style = request.keep_style
|
||||
subject = deps.clean_text(row.get("primary_subject") or row.get("subject") or "")
|
||||
if subject not in ("woman", "man"):
|
||||
return None
|
||||
|
||||
caption_front = deps.single_caption_front(row)
|
||||
age = deps.clean_text(row.get("age") or row.get("age_band") or caption_front.get("caption_age") or "")
|
||||
body_phrase = deps.field_row_value(row, "body_phrase") or caption_front.get("caption_body_phrase", "")
|
||||
if not body_phrase:
|
||||
body = deps.clean_text(row.get("body_type") or row.get("body") or "")
|
||||
figure = deps.clean_text(row.get("figure"))
|
||||
body_phrase = deps.body_phrase(body, figure)
|
||||
|
||||
skin = deps.field_row_value(row, "skin") or caption_front.get("caption_skin", "")
|
||||
hair = deps.field_row_value(row, "hair") or caption_front.get("caption_hair", "")
|
||||
eyes = deps.field_row_value(row, "eyes") or caption_front.get("caption_eyes", "")
|
||||
item = deps.row_value(row, "item", deps.item_labels)
|
||||
if item:
|
||||
item = deps.clean_clothing(item)
|
||||
if not item:
|
||||
item = deps.clean_clothing(deps.row_value(row, "clothing", ("Clothing", "Erotic outfit")))
|
||||
scene = deps.row_value(row, "scene_text", ("Scene", "Setting"))
|
||||
pose = deps.row_value(row, "pose", ("Pose",))
|
||||
expression = "" if deps.expression_disabled(row) else deps.row_value(row, "expression", ("Facial expression", "Facial expressions"))
|
||||
composition = deps.normalize_composition(deps.row_value(row, "composition", ("Composition",)))
|
||||
camera_scene = deps.clean_text(row.get("camera_scene_directive"))
|
||||
prop = deps.row_value(row, "prop", ("Prop/detail",))
|
||||
style = deps.field_row_value(row, "style") if keep_style else ""
|
||||
|
||||
parts = []
|
||||
opener = deps.age_subject(age, subject)
|
||||
appearance_details = [piece for piece in (skin, hair, eyes) if piece]
|
||||
if body_phrase:
|
||||
parts.append(f"{opener} has {deps.article(body_phrase)} {body_phrase}")
|
||||
elif appearance_details:
|
||||
parts.append(f"{opener} has {deps.human_join(appearance_details)}")
|
||||
else:
|
||||
parts.append(opener)
|
||||
if body_phrase and appearance_details:
|
||||
parts.append(f"{pronoun(subject)} has {deps.human_join(appearance_details)}")
|
||||
if item:
|
||||
verb = "wears" if subject == "woman" else "is dressed in"
|
||||
parts.append(f"{pronoun(subject)} {verb} {item}")
|
||||
if prop:
|
||||
parts.append(f"{pronoun(subject)} is {prop}")
|
||||
if pose:
|
||||
parts.append(f"{pronoun(subject)} is {deps.pose_clause(pose)}")
|
||||
if 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:
|
||||
parts.append(f"The setting is {scene}")
|
||||
if deps.detail_allows(detail_level) and camera_scene:
|
||||
parts.append(camera_scene)
|
||||
if deps.detail_allows(detail_level) and composition:
|
||||
parts.append(f"The composition is {composition}")
|
||||
if keep_style and style:
|
||||
parts.append(f"The visual style is {style}")
|
||||
return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(single)")
|
||||
|
||||
|
||||
def couple_from_row_result(
|
||||
request: CaptionMetadataRouteRequest,
|
||||
deps: CaptionMetadataRouteDependencies,
|
||||
) -> CaptionMetadataRoute | None:
|
||||
row = request.row
|
||||
detail_level = request.detail_level
|
||||
keep_style = request.keep_style
|
||||
subject = deps.clean_text(row.get("subject_phrase") or row.get("primary_subject"))
|
||||
primary = deps.clean_text(row.get("primary_subject"))
|
||||
if "couple" not in primary and subject not in ("two women", "two men", "a woman and a man"):
|
||||
if not primary.startswith("two ") and " and " not in subject:
|
||||
return None
|
||||
if subject == "woman and man":
|
||||
subject = "a woman and a man"
|
||||
|
||||
ages = deps.row_value(row, "age", ("Ages",)) or deps.clean_text(row.get("age_band"))
|
||||
body = deps.row_value(row, "body", ("Body types",)) or deps.clean_text(row.get("body_type"))
|
||||
pose = deps.row_value(row, "pose", ("Pose",))
|
||||
pose = pose.replace(", affectionate and flirtatious but non-explicit", "")
|
||||
clothing = deps.clean_clothing(deps.row_value(row, "item", deps.item_labels) or deps.row_value(row, "clothing", ("Clothing",)))
|
||||
scene = deps.row_value(row, "scene_text", ("Scene", "Setting"))
|
||||
expression = ""
|
||||
if not deps.expression_disabled(row):
|
||||
expression = deps.row_value(row, "character_expression_text") or deps.row_value(
|
||||
row,
|
||||
"expression",
|
||||
("Facial expressions", "Facial expression"),
|
||||
)
|
||||
composition = deps.normalize_composition(deps.row_value(row, "composition", ("Composition",)))
|
||||
camera_scene = deps.clean_text(row.get("camera_scene_directive"))
|
||||
style = deps.field_row_value(row, "style") if keep_style else ""
|
||||
|
||||
parts = [couple_subject_sentence(subject, ages, deps.cap_first, deps.clean_age_phrase)]
|
||||
if body:
|
||||
parts.append(f"Their body types are {body}")
|
||||
if clothing:
|
||||
parts.append(couple_clothing_sentence(clothing, deps.clean_text))
|
||||
if pose:
|
||||
parts.append(f"The pose is {pose}")
|
||||
if scene:
|
||||
parts.append(f"The setting is {scene}")
|
||||
if deps.detail_allows(detail_level) and camera_scene:
|
||||
parts.append(camera_scene)
|
||||
if 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:
|
||||
parts.append(f"The composition is {composition}")
|
||||
if keep_style and style:
|
||||
parts.append(f"The visual style is {style}")
|
||||
return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(couple)")
|
||||
|
||||
|
||||
def configured_cast_from_row_result(
|
||||
request: CaptionMetadataRouteRequest,
|
||||
deps: CaptionMetadataRouteDependencies,
|
||||
) -> CaptionMetadataRoute | None:
|
||||
row = request.row
|
||||
detail_level = request.detail_level
|
||||
keep_style = request.keep_style
|
||||
if deps.clean_text(row.get("subject_type")) != "configured_cast":
|
||||
if "hardcore sexual poses" not in deps.clean_text(row.get("main_category")).lower():
|
||||
return None
|
||||
|
||||
subject = deps.subject_phrase_from_counts(row)
|
||||
verb = deps.verb_for_row(row)
|
||||
cast = deps.row_value(row, "cast_summary", ("Cast",))
|
||||
role_graph = deps.row_value(row, "role_graph", ("Role graph",))
|
||||
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"))
|
||||
expression = ""
|
||||
if not deps.expression_disabled(row):
|
||||
expression = deps.row_value(row, "character_expression_text") or deps.row_value(
|
||||
row,
|
||||
"expression",
|
||||
("Facial expressions", "Facial expression"),
|
||||
)
|
||||
composition = deps.normalize_composition(deps.row_value(row, "composition", ("Composition",)))
|
||||
camera_scene = deps.clean_text(row.get("camera_scene_directive"))
|
||||
cast_descriptor_text = deps.row_value(row, "cast_descriptor_text", ("Characters", "Cast descriptors"))
|
||||
scene_kind = deps.field_row_value(row, "scene_kind") or "explicit adult sex scene"
|
||||
style = deps.field_row_value(row, "style") if keep_style else ""
|
||||
|
||||
parts = [f"{deps.cap_first(subject)} {verb} shown as a consensual {scene_kind}"]
|
||||
if cast_descriptor_text:
|
||||
parts.append(deps.natural_cast_descriptor_text(cast_descriptor_text))
|
||||
if cast and not cast_descriptor_text:
|
||||
parts.append(f"The cast is {cast}")
|
||||
if role_graph:
|
||||
parts.append(role_graph)
|
||||
if 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 = []
|
||||
if scene:
|
||||
scene_bits.append(f"set in {scene}")
|
||||
if 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:
|
||||
scene_bits.append(f"framed as {composition}")
|
||||
if scene_bits and deps.detail_allows(detail_level):
|
||||
parts.append(", ".join(scene_bits))
|
||||
if deps.detail_allows(detail_level) and camera_scene:
|
||||
parts.append(camera_scene)
|
||||
if keep_style and style:
|
||||
parts.append(f"The visual style is {style}")
|
||||
return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(configured_cast)")
|
||||
|
||||
|
||||
def group_or_layout_from_row_result(
|
||||
request: CaptionMetadataRouteRequest,
|
||||
deps: CaptionMetadataRouteDependencies,
|
||||
) -> CaptionMetadataRoute | None:
|
||||
row = request.row
|
||||
detail_level = request.detail_level
|
||||
keep_style = request.keep_style
|
||||
primary = deps.clean_text(row.get("primary_subject"))
|
||||
if "group" not in primary and primary != "layout scene":
|
||||
return None
|
||||
|
||||
subject = deps.field_row_value(row, "subject_phrase") or primary
|
||||
age = deps.row_value(row, "age", ("Ages",)) or deps.clean_text(row.get("age_band"))
|
||||
item = deps.clean_clothing(deps.row_value(row, "item", deps.item_labels) or deps.row_value(row, "clothing", ("Clothing",)))
|
||||
scene = deps.row_value(row, "scene_text", ("Scene", "Setting"))
|
||||
expression = ""
|
||||
if not deps.expression_disabled(row):
|
||||
expression = deps.row_value(row, "character_expression_text") or deps.row_value(
|
||||
row,
|
||||
"expression",
|
||||
("Facial expressions", "Facial expression"),
|
||||
)
|
||||
composition = deps.normalize_composition(deps.row_value(row, "composition", ("Composition",)))
|
||||
camera_scene = deps.clean_text(row.get("camera_scene_directive"))
|
||||
style = deps.field_row_value(row, "style") if keep_style else ""
|
||||
|
||||
if primary == "layout scene":
|
||||
parts = [f"{deps.cap_first(subject)} is arranged as an adults-only designed illustration layout"]
|
||||
if 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:
|
||||
parts = [f"{deps.cap_first(subject)} includes adults"]
|
||||
if age:
|
||||
parts[0] += f" ages {age}"
|
||||
if item:
|
||||
parts.append(f"They wear {item}")
|
||||
if 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:
|
||||
parts.append(f"The setting is {scene}")
|
||||
if deps.detail_allows(detail_level) and camera_scene:
|
||||
parts.append(camera_scene)
|
||||
if deps.detail_allows(detail_level) and composition:
|
||||
parts.append(f"The composition is {composition}")
|
||||
if keep_style and style:
|
||||
parts.append(f"The visual style is {style}")
|
||||
return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(group_layout)")
|
||||
|
||||
|
||||
def insta_of_pair_from_row_result(
|
||||
request: CaptionMetadataRouteRequest,
|
||||
deps: CaptionMetadataRouteDependencies,
|
||||
) -> CaptionMetadataRoute | None:
|
||||
row = request.row
|
||||
detail_level = request.detail_level
|
||||
keep_style = request.keep_style
|
||||
pair_target = target_policy.pair_policy(request.target)
|
||||
target = pair_target.pair_target
|
||||
if not input_policy.is_pair_metadata(row):
|
||||
return None
|
||||
soft_row = row.get("softcore_row")
|
||||
hard_row = row.get("hardcore_row")
|
||||
if not isinstance(soft_row, dict) or not isinstance(hard_row, dict):
|
||||
return None
|
||||
|
||||
hard_row_for_text = dict(hard_row)
|
||||
options = row.get("options")
|
||||
if isinstance(options, dict) and options.get("continuity") == "same_creator_same_room":
|
||||
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"]
|
||||
if not hard_row_for_text.get("composition") and soft_row.get("composition"):
|
||||
hard_row_for_text["composition"] = soft_row["composition"]
|
||||
|
||||
include_soft = pair_target.include_softcore
|
||||
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"))
|
||||
options = row.get("options") if isinstance(row.get("options"), dict) else {}
|
||||
cast_descriptors = row.get("shared_cast_descriptors")
|
||||
if isinstance(cast_descriptors, list):
|
||||
cast_descriptor_text = "; ".join(deps.clean_text(item) for item in cast_descriptors if deps.clean_text(item))
|
||||
else:
|
||||
cast_descriptor_text = deps.clean_text(cast_descriptors)
|
||||
labels = deps.cast_labels(cast_descriptor_text)
|
||||
|
||||
same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"
|
||||
|
||||
parts = []
|
||||
if not soft_text and not hard_text:
|
||||
if cast_descriptor_text:
|
||||
parts.append(deps.natural_cast_descriptor_text(cast_descriptor_text))
|
||||
elif descriptor:
|
||||
parts.append(f"A {descriptor}")
|
||||
if same_soft_cast and include_soft:
|
||||
parts.append(
|
||||
deps.softcore_caption_setup_phrase(
|
||||
same_cast=True,
|
||||
target_auto=target == "auto",
|
||||
)
|
||||
)
|
||||
partner_styling = row.get("softcore_partner_styling")
|
||||
if isinstance(partner_styling, dict):
|
||||
outfits = partner_styling.get("outfits")
|
||||
if isinstance(outfits, list):
|
||||
outfit_text = deps.human_join([deps.clean_text(item) for item in outfits if deps.clean_text(item)])
|
||||
outfit_text = deps.natural_label_text(outfit_text, labels)
|
||||
if outfit_text:
|
||||
parts.append(f"Softcore partner styling: {outfit_text}")
|
||||
pose = deps.clean_text(partner_styling.get("pose"))
|
||||
if pose:
|
||||
parts.append(f"The shared softcore cast pose is {pose}")
|
||||
if soft_text:
|
||||
parts.append(f"Softcore side: {soft_text}" if target == "auto" else soft_text)
|
||||
if hard_text:
|
||||
parts.append(f"Hardcore side: {hard_text}" if target == "auto" else hard_text)
|
||||
if not parts:
|
||||
return None
|
||||
return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(insta_of_pair)")
|
||||
|
||||
|
||||
def single_from_row(request: CaptionMetadataRouteRequest, deps: CaptionMetadataRouteDependencies) -> tuple[str, str] | None:
|
||||
result = single_from_row_result(request, deps)
|
||||
return result.as_tuple() if result else None
|
||||
|
||||
|
||||
def couple_from_row(request: CaptionMetadataRouteRequest, deps: CaptionMetadataRouteDependencies) -> tuple[str, str] | None:
|
||||
result = couple_from_row_result(request, deps)
|
||||
return result.as_tuple() if result else None
|
||||
|
||||
|
||||
def configured_cast_from_row(
|
||||
request: CaptionMetadataRouteRequest,
|
||||
deps: CaptionMetadataRouteDependencies,
|
||||
) -> tuple[str, str] | None:
|
||||
result = configured_cast_from_row_result(request, deps)
|
||||
return result.as_tuple() if result else None
|
||||
|
||||
|
||||
def group_or_layout_from_row(
|
||||
request: CaptionMetadataRouteRequest,
|
||||
deps: CaptionMetadataRouteDependencies,
|
||||
) -> tuple[str, str] | None:
|
||||
result = group_or_layout_from_row_result(request, deps)
|
||||
return result.as_tuple() if result else None
|
||||
|
||||
|
||||
def insta_of_pair_from_row(
|
||||
request: CaptionMetadataRouteRequest,
|
||||
deps: CaptionMetadataRouteDependencies,
|
||||
) -> tuple[str, str] | None:
|
||||
result = insta_of_pair_from_row_result(request, deps)
|
||||
return result.as_tuple() if result else None
|
||||
+201
-511
@@ -1,628 +1,265 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from . import caption_format_route
|
||||
from . import caption_metadata_routes
|
||||
from . import caption_policy
|
||||
from . import caption_text_policy
|
||||
from . import formatter_input as input_policy
|
||||
from .prompt_hygiene import sanitize_prose_text
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
import caption_format_route
|
||||
import caption_metadata_routes
|
||||
import caption_policy
|
||||
import caption_text_policy
|
||||
import formatter_input as input_policy
|
||||
from prompt_hygiene import sanitize_prose_text
|
||||
|
||||
|
||||
OLD_TRIGGER = "sxcpinup_coloredpencil"
|
||||
DEFAULT_TRIGGER = "sxcppnl7"
|
||||
OLD_TRIGGER = caption_policy.OLD_TRIGGER
|
||||
DEFAULT_TRIGGER = caption_policy.DEFAULT_TRIGGER
|
||||
STYLE_TAILS = caption_policy.STYLE_TAILS
|
||||
|
||||
STYLE_TAILS = [
|
||||
", coloured pencil comic illustration, crisp linework, hatching, soft pastel palette, warm sensual lighting, textured parchment paper",
|
||||
", coloured pencil comic illustration, crisp linework, hatching, soft pastel palette, warm sensual lighting, textured paper",
|
||||
]
|
||||
|
||||
PROMPT_FIELD_LABELS = (
|
||||
"Ages",
|
||||
"Body types",
|
||||
"Cast",
|
||||
"Cast descriptors",
|
||||
"Characters",
|
||||
"Scene",
|
||||
"Setting",
|
||||
"Pose",
|
||||
"Sexual pose",
|
||||
"Facial expression",
|
||||
"Facial expressions",
|
||||
"Clothing",
|
||||
"Erotic outfit",
|
||||
"Prop/detail",
|
||||
"Composition",
|
||||
"Role graph",
|
||||
"Use",
|
||||
"Avoid",
|
||||
)
|
||||
|
||||
ITEM_LABELS = (
|
||||
"Sexual pose",
|
||||
"Erotic outfit",
|
||||
"Clothing",
|
||||
)
|
||||
PROMPT_FIELD_LABELS = caption_text_policy.PROMPT_FIELD_LABELS
|
||||
ITEM_LABELS = caption_policy.ITEM_LABELS
|
||||
ACTION_FAMILY_CAPTION_LABELS = caption_policy.ACTION_FAMILY_CAPTION_LABELS
|
||||
POSITION_FAMILY_CAPTION_LABELS = caption_policy.POSITION_FAMILY_CAPTION_LABELS
|
||||
|
||||
|
||||
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
|
||||
return caption_text_policy.clean_text(value)
|
||||
|
||||
|
||||
def _is_false(value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value is False
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ("false", "0", "no", "off")
|
||||
return False
|
||||
return caption_text_policy.is_false(value)
|
||||
|
||||
|
||||
def _expression_disabled(row: dict[str, Any]) -> bool:
|
||||
return bool(row.get("expression_disabled")) or _is_false(row.get("expression_enabled", True))
|
||||
return caption_text_policy.expression_disabled(row)
|
||||
|
||||
|
||||
def _cap_first(text: str) -> str:
|
||||
text = _clean_text(text).strip(" ,")
|
||||
return text[:1].upper() + text[1:] if text else ""
|
||||
return caption_text_policy.cap_first(text)
|
||||
|
||||
|
||||
def _article(noun_phrase: str) -> str:
|
||||
word = noun_phrase.lstrip().lower()
|
||||
if word.startswith("hour") or word[:1] in "aeiou":
|
||||
return "an"
|
||||
return "a"
|
||||
return caption_text_policy.article(noun_phrase)
|
||||
|
||||
|
||||
def _sentence(text: str) -> str:
|
||||
text = _clean_text(text).strip(" ,;")
|
||||
if not text:
|
||||
return ""
|
||||
if text[-1] not in ".!?":
|
||||
text += "."
|
||||
return _cap_first(text)
|
||||
return caption_text_policy.sentence(text)
|
||||
|
||||
|
||||
def _join_sentences(parts: list[str]) -> str:
|
||||
return " ".join(part for part in (_sentence(part) for part in parts) if part)
|
||||
return caption_text_policy.join_sentences(parts)
|
||||
|
||||
|
||||
def _formatter_hint_parts(row: dict[str, Any]) -> list[str]:
|
||||
return caption_text_policy.formatter_hint_parts(row)
|
||||
|
||||
|
||||
def _append_formatter_hints(prose: str, row: dict[str, Any]) -> str:
|
||||
return caption_text_policy.append_formatter_hints(prose, row)
|
||||
|
||||
|
||||
def _human_join(parts: list[str]) -> str:
|
||||
parts = [part for part in (_clean_text(part) for part in parts) if part]
|
||||
if len(parts) <= 1:
|
||||
return "".join(parts)
|
||||
if len(parts) == 2:
|
||||
return f"{parts[0]} and {parts[1]}"
|
||||
return f"{', '.join(parts[:-1])}, and {parts[-1]}"
|
||||
return caption_text_policy.human_join(parts)
|
||||
|
||||
|
||||
def _metadata_action_label(row: dict[str, Any], default: str = "sexual pose") -> str:
|
||||
return caption_text_policy.metadata_action_label(row, default)
|
||||
|
||||
|
||||
def _prompt_cast_descriptors(text: str) -> str:
|
||||
return _clean_text(text).replace("Woman A / primary creator:", "Woman A:")
|
||||
return caption_text_policy.prompt_cast_descriptors(text)
|
||||
|
||||
|
||||
def _cast_entries(text: str) -> list[tuple[str, str]]:
|
||||
text = _prompt_cast_descriptors(text)
|
||||
entries: list[tuple[str, str]] = []
|
||||
for part in text.split(";"):
|
||||
part = _clean_text(part)
|
||||
match = re.match(r"^((?:Woman|Man) [A-Z]):\s*(.+)$", part)
|
||||
if match:
|
||||
entries.append((match.group(1), _clean_text(match.group(2))))
|
||||
return entries
|
||||
return caption_text_policy.cast_entries(text)
|
||||
|
||||
|
||||
def _natural_cast_descriptor_text(text: str) -> str:
|
||||
entries = _cast_entries(text)
|
||||
if not entries:
|
||||
return _clean_text(text)
|
||||
labels = [label for label, _descriptor in entries]
|
||||
if labels == ["Woman A"] or labels == ["Man A"]:
|
||||
return f"A {entries[0][1]}"
|
||||
if set(labels) == {"Woman A", "Man A"} and len(labels) == 2:
|
||||
by_label = {label: descriptor for label, descriptor in entries}
|
||||
return f"A {by_label['Woman A']} alongside a {by_label['Man A']}"
|
||||
return " ".join(f"{label} is {descriptor}." for label, descriptor in entries)
|
||||
return caption_text_policy.natural_cast_descriptor_text(text)
|
||||
|
||||
|
||||
def _cast_labels(text: str) -> list[str]:
|
||||
return [label for label, _descriptor in _cast_entries(text)]
|
||||
return caption_text_policy.cast_labels(text)
|
||||
|
||||
|
||||
def _natural_label_text(text: Any, labels: list[str]) -> str:
|
||||
text = _clean_text(text)
|
||||
if not text:
|
||||
return ""
|
||||
if set(labels) == {"Woman A", "Man A"}:
|
||||
text = re.sub(r"\bWoman A\b", "the woman", text)
|
||||
text = re.sub(r"\bMan A\b", "the man", text)
|
||||
elif labels == ["Woman A"]:
|
||||
text = re.sub(r"\bWoman A\b", "the woman", text)
|
||||
elif labels == ["Man A"]:
|
||||
text = re.sub(r"\bMan A\b", "the man", text)
|
||||
return text
|
||||
return caption_text_policy.natural_label_text(text, labels)
|
||||
|
||||
|
||||
def _strip_style_tail(text: str) -> str:
|
||||
text = _clean_text(text)
|
||||
for tail in STYLE_TAILS:
|
||||
if text.endswith(tail):
|
||||
return text[: -len(tail)].strip(" ,")
|
||||
return text
|
||||
return caption_text_policy.strip_style_tail(text)
|
||||
|
||||
|
||||
def _remove_trigger(text: str, trigger: str) -> str:
|
||||
text = _clean_text(text).strip(" ,")
|
||||
for candidate in (trigger, OLD_TRIGGER, DEFAULT_TRIGGER):
|
||||
candidate = candidate.strip()
|
||||
if not candidate:
|
||||
continue
|
||||
if text.lower().startswith(candidate.lower() + ","):
|
||||
return text[len(candidate) + 1 :].strip(" ,")
|
||||
if text.lower().startswith(candidate.lower() + "."):
|
||||
return text[len(candidate) + 1 :].strip(" ,")
|
||||
if text.lower() == candidate.lower():
|
||||
return ""
|
||||
return text
|
||||
return caption_text_policy.remove_trigger(text, trigger)
|
||||
|
||||
|
||||
def _with_trigger(text: str, trigger: str, include_trigger: bool) -> str:
|
||||
text = _join_sentences([text]) if "." not in text else _clean_text(text)
|
||||
trigger = _clean_text(trigger or DEFAULT_TRIGGER)
|
||||
if not include_trigger or not trigger:
|
||||
return text
|
||||
if text.lower().startswith(trigger.lower() + "."):
|
||||
return text
|
||||
return f"{trigger}. {text}"
|
||||
return caption_text_policy.with_trigger(text, trigger, include_trigger)
|
||||
|
||||
|
||||
def _maybe_json(text: str) -> dict[str, Any] | None:
|
||||
text = _clean_text(text)
|
||||
if not text or not text.startswith("{"):
|
||||
return None
|
||||
try:
|
||||
value = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return value if isinstance(value, dict) else None
|
||||
return input_policy.maybe_json(text)
|
||||
|
||||
|
||||
def _row_from_inputs(source_text: str, metadata_json: str, input_hint: str) -> tuple[dict[str, Any] | None, str]:
|
||||
candidates: list[tuple[str, str]] = []
|
||||
if input_hint in ("auto", "metadata_json"):
|
||||
candidates.append((metadata_json, "metadata_json"))
|
||||
candidates.append((source_text, "source_json"))
|
||||
for text, method in candidates:
|
||||
row = _maybe_json(text)
|
||||
if row is not None:
|
||||
return row, method
|
||||
return None, "text"
|
||||
return input_policy.row_from_inputs(source_text, metadata_json, input_hint)
|
||||
|
||||
|
||||
def _prompt_field(text: str, label: str) -> str:
|
||||
text = _clean_text(text)
|
||||
if not text:
|
||||
return ""
|
||||
labels = "|".join(re.escape(name) for name in PROMPT_FIELD_LABELS)
|
||||
pattern = rf"{re.escape(label)}:\s*(.*?)(?=\. (?:{labels}):|\. Use\b|\. Avoid\b|$)"
|
||||
match = re.search(pattern, text)
|
||||
if not match:
|
||||
return ""
|
||||
return _clean_text(match.group(1)).rstrip(".")
|
||||
return caption_text_policy.prompt_field(text, label)
|
||||
|
||||
|
||||
def _row_value(row: dict[str, Any], key: str, labels: tuple[str, ...] = ()) -> str:
|
||||
value = _clean_text(row.get(key, ""))
|
||||
if value:
|
||||
return value
|
||||
prompt = _clean_text(row.get("prompt", ""))
|
||||
for label in labels:
|
||||
value = _prompt_field(prompt, label)
|
||||
if value:
|
||||
return value
|
||||
return ""
|
||||
return caption_text_policy.row_value(row, key, labels)
|
||||
|
||||
|
||||
def _field_from_any_prompt(text: str, labels: tuple[str, ...]) -> str:
|
||||
for label in labels:
|
||||
value = _prompt_field(text, label)
|
||||
if value:
|
||||
return value
|
||||
return ""
|
||||
return caption_text_policy.field_from_any_prompt(text, labels)
|
||||
|
||||
|
||||
def _normalize_composition(text: str) -> str:
|
||||
return re.sub(r"^vertical\s+", "", _clean_text(text), flags=re.IGNORECASE)
|
||||
return caption_text_policy.normalize_composition(text)
|
||||
|
||||
|
||||
def _clean_clothing(text: str) -> str:
|
||||
text = _clean_text(text)
|
||||
text = re.sub(r",?\s*fashion editorial styling$", "", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r",?\s*resort styling$", "", text, flags=re.IGNORECASE)
|
||||
return text.strip(" ,")
|
||||
return caption_text_policy.clean_clothing(text)
|
||||
|
||||
|
||||
def _body_phrase(body: Any, figure_note: Any = "") -> str:
|
||||
body = _clean_text(body)
|
||||
figure_note = _clean_text(figure_note)
|
||||
if not body:
|
||||
return figure_note
|
||||
if not figure_note:
|
||||
return f"{body} figure"
|
||||
if "figure" in figure_note.lower():
|
||||
return f"{body} build and {figure_note}"
|
||||
return f"{body} figure with {figure_note}"
|
||||
return caption_text_policy.body_phrase(body, figure_note)
|
||||
|
||||
|
||||
def _single_caption_front(row: dict[str, Any]) -> dict[str, str]:
|
||||
caption = _clean_text(row.get("caption"))
|
||||
if not caption:
|
||||
return {}
|
||||
caption = _remove_trigger(_strip_style_tail(caption), _clean_text(row.get("trigger")) or DEFAULT_TRIGGER)
|
||||
caption = _remove_trigger(caption, OLD_TRIGGER)
|
||||
subject = _clean_text(row.get("primary_subject"))
|
||||
age = _clean_text(row.get("age_band") or row.get("age"))
|
||||
body_phrase = _clean_text(row.get("body_phrase"))
|
||||
if not body_phrase:
|
||||
body = _clean_text(row.get("body_type") or row.get("body"))
|
||||
figure = _clean_text(row.get("figure"))
|
||||
body_phrase = _body_phrase(body, figure)
|
||||
front = f"{subject}, {age}, {body_phrase}, "
|
||||
if subject in ("woman", "man") and age and body_phrase and caption.startswith(front):
|
||||
try:
|
||||
skin, hair, eyes, _rest = caption[len(front) :].split(", ", 3)
|
||||
except ValueError:
|
||||
return {}
|
||||
else:
|
||||
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 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,
|
||||
}
|
||||
return caption_text_policy.single_caption_front(row)
|
||||
|
||||
|
||||
def _pose_clause(pose: str) -> str:
|
||||
pose = _clean_text(pose)
|
||||
if not pose:
|
||||
return ""
|
||||
first = pose.split(None, 1)[0].lower()
|
||||
if first.endswith("ing") or first in ("seated", "reclined", "posed"):
|
||||
return pose
|
||||
return f"posing in {pose}"
|
||||
return caption_text_policy.pose_clause(pose)
|
||||
|
||||
|
||||
def _age_subject(age: str, subject: str) -> str:
|
||||
age = _clean_text(age)
|
||||
subject = _clean_text(subject) or "person"
|
||||
if not age:
|
||||
return f"An adult {subject}"
|
||||
clean_age = re.sub(r"\s+adults?$", "", age).strip()
|
||||
if "year-old" in clean_age:
|
||||
return f"A {clean_age} adult {subject}"
|
||||
if re.search(r"\d", clean_age):
|
||||
poss = "her" if subject == "woman" else "his"
|
||||
return f"An adult {subject} in {poss} {clean_age}"
|
||||
return f"An adult {clean_age} {subject}"
|
||||
return caption_text_policy.age_subject(age, subject)
|
||||
|
||||
|
||||
def _clean_age_phrase(age: str) -> str:
|
||||
age = _clean_text(age)
|
||||
age = re.sub(r"\s+adults?$", "", age).strip()
|
||||
return age.replace("-year-old", " years old")
|
||||
return caption_text_policy.clean_age_phrase(age)
|
||||
|
||||
|
||||
def _subject_phrase_from_counts(row: dict[str, Any]) -> str:
|
||||
subject = _clean_text(row.get("subject_phrase"))
|
||||
if subject:
|
||||
return subject
|
||||
try:
|
||||
women = int(row.get("women_count") or 0)
|
||||
men = int(row.get("men_count") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return _clean_text(row.get("primary_subject")) or "adult scene"
|
||||
parts = []
|
||||
if women:
|
||||
parts.append(f"{women} adult {'woman' if women == 1 else 'women'}")
|
||||
if men:
|
||||
parts.append(f"{men} adult {'man' if men == 1 else 'men'}")
|
||||
if not parts:
|
||||
return _clean_text(row.get("primary_subject")) or "adult scene"
|
||||
return " and ".join(parts)
|
||||
return caption_text_policy.subject_phrase_from_counts(row)
|
||||
|
||||
|
||||
def _verb_for_row(row: dict[str, Any]) -> str:
|
||||
try:
|
||||
return "is" if int(row.get("person_count") or 0) == 1 else "are"
|
||||
except (TypeError, ValueError):
|
||||
return "are"
|
||||
return caption_text_policy.verb_for_row(row)
|
||||
|
||||
|
||||
def _detail_allows(level: str, dense_only: bool = False) -> bool:
|
||||
level = (level or "balanced").strip().lower()
|
||||
if dense_only:
|
||||
return level == "dense"
|
||||
return level != "concise"
|
||||
return caption_text_policy.detail_allows(level, dense_only=dense_only)
|
||||
|
||||
|
||||
def _single_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None:
|
||||
subject = _clean_text(row.get("primary_subject") or row.get("subject") or "")
|
||||
if subject not in ("woman", "man"):
|
||||
return None
|
||||
def _caption_metadata_route_dependencies() -> caption_metadata_routes.CaptionMetadataRouteDependencies:
|
||||
return caption_text_policy.metadata_route_dependencies(_metadata_to_prose)
|
||||
|
||||
caption_front = _single_caption_front(row)
|
||||
age = _clean_text(row.get("age") or row.get("age_band") or caption_front.get("caption_age") or "")
|
||||
body_phrase = _row_value(row, "body_phrase") or caption_front.get("caption_body_phrase", "")
|
||||
if not body_phrase:
|
||||
body = _clean_text(row.get("body_type") or row.get("body") or "")
|
||||
figure = _clean_text(row.get("figure"))
|
||||
body_phrase = _body_phrase(body, figure)
|
||||
|
||||
skin = _row_value(row, "skin") or caption_front.get("caption_skin", "")
|
||||
hair = _row_value(row, "hair") or caption_front.get("caption_hair", "")
|
||||
eyes = _row_value(row, "eyes") or caption_front.get("caption_eyes", "")
|
||||
item = _row_value(row, "item", ITEM_LABELS)
|
||||
if item:
|
||||
item = _clean_clothing(item)
|
||||
if not item:
|
||||
item = _clean_clothing(_row_value(row, "clothing", ("Clothing", "Erotic outfit")))
|
||||
scene = _row_value(row, "scene_text", ("Scene", "Setting"))
|
||||
pose = _row_value(row, "pose", ("Pose",))
|
||||
expression = "" if _expression_disabled(row) else _row_value(row, "expression", ("Facial expression", "Facial expressions"))
|
||||
composition = _normalize_composition(_row_value(row, "composition", ("Composition",)))
|
||||
camera_scene = _clean_text(row.get("camera_scene_directive"))
|
||||
prop = _row_value(row, "prop", ("Prop/detail",))
|
||||
style = _row_value(row, "style") if keep_style else ""
|
||||
def _caption_metadata_route_request(
|
||||
row: dict[str, Any],
|
||||
detail_level: str,
|
||||
keep_style: bool,
|
||||
target: str = "auto",
|
||||
) -> caption_metadata_routes.CaptionMetadataRouteRequest:
|
||||
return caption_metadata_routes.CaptionMetadataRouteRequest(
|
||||
row=row,
|
||||
detail_level=detail_level,
|
||||
keep_style=keep_style,
|
||||
target=target,
|
||||
)
|
||||
|
||||
parts = []
|
||||
opener = _age_subject(age, subject)
|
||||
appearance_details = [piece for piece in (skin, hair, eyes) if piece]
|
||||
if body_phrase:
|
||||
parts.append(f"{opener} has {_article(body_phrase)} {body_phrase}")
|
||||
elif appearance_details:
|
||||
parts.append(f"{opener} has {_human_join(appearance_details)}")
|
||||
else:
|
||||
parts.append(opener)
|
||||
if body_phrase and appearance_details:
|
||||
parts.append(f"{pronoun(subject)} has {_human_join(appearance_details)}")
|
||||
if item:
|
||||
verb = "wears" if subject == "woman" else "is dressed in"
|
||||
parts.append(f"{pronoun(subject)} {verb} {item}")
|
||||
if prop:
|
||||
parts.append(f"{pronoun(subject)} is {prop}")
|
||||
if pose:
|
||||
parts.append(f"{pronoun(subject)} is {_pose_clause(pose)}")
|
||||
if expression:
|
||||
parts.append(f"{possessive_pronoun(subject)} expression is {expression}")
|
||||
if scene:
|
||||
parts.append(f"The setting is {scene}")
|
||||
if _detail_allows(detail_level) and camera_scene:
|
||||
parts.append(camera_scene)
|
||||
if _detail_allows(detail_level) and composition:
|
||||
parts.append(f"The composition is {composition}")
|
||||
if keep_style and style:
|
||||
parts.append(f"The visual style is {style}")
|
||||
return _join_sentences(parts), "metadata(single)"
|
||||
|
||||
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(
|
||||
_caption_metadata_route_request(row, detail_level, keep_style, target),
|
||||
_caption_metadata_route_dependencies(),
|
||||
)
|
||||
|
||||
|
||||
def pronoun(subject: str) -> str:
|
||||
return "She" if subject == "woman" else "He"
|
||||
return caption_metadata_routes.pronoun(subject)
|
||||
|
||||
|
||||
def possessive_pronoun(subject: str) -> str:
|
||||
return "Her" if subject == "woman" else "His"
|
||||
return caption_metadata_routes.possessive_pronoun(subject)
|
||||
|
||||
|
||||
def _couple_clothing_sentence(clothing: str) -> str:
|
||||
clothing = _clean_text(clothing)
|
||||
lower = clothing.lower()
|
||||
partner_text = re.sub(r"\bPartner ([AB]) wears\b", r"Partner \1 wearing", clothing)
|
||||
partner_text = re.sub(r"\bPartner ([AB]) has\b", r"Partner \1 with", partner_text)
|
||||
if lower.startswith("partner a "):
|
||||
return f"The outfits show {partner_text}"
|
||||
if lower.startswith(("two ", "paired ", "coordinated ")):
|
||||
return f"The outfits are {partner_text}"
|
||||
return f"They wear {clothing}"
|
||||
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:
|
||||
subject = _clean_text(row.get("subject_phrase") or row.get("primary_subject"))
|
||||
primary = _clean_text(row.get("primary_subject"))
|
||||
if "couple" not in primary and subject not in ("two women", "two men", "a woman and a man"):
|
||||
if not primary.startswith("two ") and " and " not in subject:
|
||||
return None
|
||||
if subject == "woman and man":
|
||||
subject = "a woman and a man"
|
||||
|
||||
ages = _row_value(row, "age", ("Ages",)) or _clean_text(row.get("age_band"))
|
||||
body = _row_value(row, "body", ("Body types",)) or _clean_text(row.get("body_type"))
|
||||
pose = _row_value(row, "pose", ("Pose",))
|
||||
pose = pose.replace(", affectionate and flirtatious but non-explicit", "")
|
||||
clothing = _clean_clothing(_row_value(row, "item", ITEM_LABELS) or _row_value(row, "clothing", ("Clothing",)))
|
||||
scene = _row_value(row, "scene_text", ("Scene", "Setting"))
|
||||
expression = ""
|
||||
if not _expression_disabled(row):
|
||||
expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression"))
|
||||
composition = _normalize_composition(_row_value(row, "composition", ("Composition",)))
|
||||
camera_scene = _clean_text(row.get("camera_scene_directive"))
|
||||
style = _row_value(row, "style") if keep_style else ""
|
||||
|
||||
parts = [f"{_cap_first(subject)} are adults"]
|
||||
if ages:
|
||||
parts.append(f"The age detail is {_clean_age_phrase(ages)}")
|
||||
if body:
|
||||
parts.append(f"Their body types are {body}")
|
||||
if clothing:
|
||||
parts.append(_couple_clothing_sentence(clothing))
|
||||
if pose:
|
||||
parts.append(f"The pose is {pose}")
|
||||
if scene:
|
||||
parts.append(f"The setting is {scene}")
|
||||
if _detail_allows(detail_level) and camera_scene:
|
||||
parts.append(camera_scene)
|
||||
if expression:
|
||||
parts.append(f"Their expressions are {expression}")
|
||||
if _detail_allows(detail_level) and composition:
|
||||
parts.append(f"The composition is {composition}")
|
||||
if keep_style and style:
|
||||
parts.append(f"The visual style is {style}")
|
||||
return _join_sentences(parts), "metadata(couple)"
|
||||
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(
|
||||
_caption_metadata_route_request(row, detail_level, keep_style, target),
|
||||
_caption_metadata_route_dependencies(),
|
||||
)
|
||||
|
||||
|
||||
def _configured_cast_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None:
|
||||
if _clean_text(row.get("subject_type")) != "configured_cast":
|
||||
if "hardcore sexual poses" not in _clean_text(row.get("main_category")).lower():
|
||||
return None
|
||||
|
||||
subject = _subject_phrase_from_counts(row)
|
||||
verb = _verb_for_row(row)
|
||||
cast = _row_value(row, "cast_summary", ("Cast",))
|
||||
role_graph = _row_value(row, "role_graph", ("Role graph",))
|
||||
item = _row_value(row, "item", ITEM_LABELS)
|
||||
scene = _row_value(row, "scene_text", ("Setting", "Scene"))
|
||||
expression = ""
|
||||
if not _expression_disabled(row):
|
||||
expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression"))
|
||||
composition = _normalize_composition(_row_value(row, "composition", ("Composition",)))
|
||||
camera_scene = _clean_text(row.get("camera_scene_directive"))
|
||||
cast_descriptor_text = _row_value(row, "cast_descriptor_text", ("Characters", "Cast descriptors"))
|
||||
scene_kind = _row_value(row, "scene_kind") or "explicit adult sex scene"
|
||||
style = _row_value(row, "style") if keep_style else ""
|
||||
|
||||
parts = [f"{_cap_first(subject)} {verb} shown as a consensual {scene_kind}"]
|
||||
if cast_descriptor_text:
|
||||
parts.append(_natural_cast_descriptor_text(cast_descriptor_text))
|
||||
if cast and not cast_descriptor_text:
|
||||
parts.append(f"The cast is {cast}")
|
||||
if role_graph:
|
||||
parts.append(role_graph)
|
||||
if item:
|
||||
parts.append(f"The sexual pose is {item}")
|
||||
scene_bits = []
|
||||
if scene:
|
||||
scene_bits.append(f"set in {scene}")
|
||||
if expression:
|
||||
scene_bits.append(f"with {expression}")
|
||||
if composition:
|
||||
scene_bits.append(f"framed as {composition}")
|
||||
if scene_bits and _detail_allows(detail_level):
|
||||
parts.append(", ".join(scene_bits))
|
||||
if _detail_allows(detail_level) and camera_scene:
|
||||
parts.append(camera_scene)
|
||||
if keep_style and style:
|
||||
parts.append(f"The visual style is {style}")
|
||||
return _join_sentences(parts), "metadata(configured_cast)"
|
||||
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(
|
||||
_caption_metadata_route_request(row, detail_level, keep_style, target),
|
||||
_caption_metadata_route_dependencies(),
|
||||
)
|
||||
|
||||
|
||||
def _group_or_layout_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None:
|
||||
primary = _clean_text(row.get("primary_subject"))
|
||||
if "group" not in primary and primary != "layout scene":
|
||||
return None
|
||||
|
||||
subject = _row_value(row, "subject_phrase") or primary
|
||||
age = _row_value(row, "age", ("Ages",)) or _clean_text(row.get("age_band"))
|
||||
item = _clean_clothing(_row_value(row, "item", ITEM_LABELS) or _row_value(row, "clothing", ("Clothing",)))
|
||||
scene = _row_value(row, "scene_text", ("Scene", "Setting"))
|
||||
expression = ""
|
||||
if not _expression_disabled(row):
|
||||
expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression"))
|
||||
composition = _normalize_composition(_row_value(row, "composition", ("Composition",)))
|
||||
camera_scene = _clean_text(row.get("camera_scene_directive"))
|
||||
style = _row_value(row, "style") if keep_style else ""
|
||||
|
||||
if primary == "layout scene":
|
||||
parts = [f"{_cap_first(subject)} is arranged as an adults-only designed illustration layout"]
|
||||
if expression:
|
||||
parts.append(f"The featured expression is {expression}")
|
||||
else:
|
||||
parts = [f"{_cap_first(subject)} includes adults"]
|
||||
if age:
|
||||
parts[0] += f" ages {age}"
|
||||
if item:
|
||||
parts.append(f"They wear {item}")
|
||||
if expression:
|
||||
parts.append(f"They show {expression}")
|
||||
if scene:
|
||||
parts.append(f"The setting is {scene}")
|
||||
if _detail_allows(detail_level) and camera_scene:
|
||||
parts.append(camera_scene)
|
||||
if _detail_allows(detail_level) and composition:
|
||||
parts.append(f"The composition is {composition}")
|
||||
if keep_style and style:
|
||||
parts.append(f"The visual style is {style}")
|
||||
return _join_sentences(parts), "metadata(group_layout)"
|
||||
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(
|
||||
_caption_metadata_route_request(row, detail_level, keep_style, target),
|
||||
_caption_metadata_route_dependencies(),
|
||||
)
|
||||
|
||||
|
||||
def _insta_of_pair_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None:
|
||||
if _clean_text(row.get("mode")).lower() != "insta/of":
|
||||
return None
|
||||
soft_row = row.get("softcore_row")
|
||||
hard_row = row.get("hardcore_row")
|
||||
if not isinstance(soft_row, dict) or not isinstance(hard_row, dict):
|
||||
return None
|
||||
|
||||
hard_row_for_text = dict(hard_row)
|
||||
options = row.get("options")
|
||||
if isinstance(options, dict) and options.get("continuity") == "same_creator_same_room":
|
||||
if soft_row.get("scene_text"):
|
||||
hard_row_for_text["scene_text"] = soft_row["scene_text"]
|
||||
if soft_row.get("composition"):
|
||||
hard_row_for_text["composition"] = soft_row["composition"]
|
||||
|
||||
soft_text, _soft_method = _metadata_to_prose(soft_row, detail_level, keep_style)
|
||||
hard_text, _hard_method = _metadata_to_prose(hard_row_for_text, detail_level, keep_style)
|
||||
descriptor = _clean_text(row.get("shared_descriptor"))
|
||||
options = row.get("options") if isinstance(row.get("options"), dict) else {}
|
||||
cast_descriptors = row.get("shared_cast_descriptors")
|
||||
if isinstance(cast_descriptors, list):
|
||||
cast_descriptor_text = "; ".join(_clean_text(item) for item in cast_descriptors if _clean_text(item))
|
||||
else:
|
||||
cast_descriptor_text = _clean_text(cast_descriptors)
|
||||
labels = _cast_labels(cast_descriptor_text)
|
||||
|
||||
same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"
|
||||
|
||||
parts = []
|
||||
if cast_descriptor_text and same_soft_cast:
|
||||
parts.append(_natural_cast_descriptor_text(cast_descriptor_text))
|
||||
elif descriptor:
|
||||
parts.append(f"A {descriptor}")
|
||||
if cast_descriptor_text and not same_soft_cast:
|
||||
parts.append(_natural_cast_descriptor_text(cast_descriptor_text))
|
||||
if same_soft_cast:
|
||||
parts.append("The softcore version keeps the same adult cast present together in a non-explicit teaser setup")
|
||||
partner_styling = row.get("softcore_partner_styling")
|
||||
if isinstance(partner_styling, dict):
|
||||
outfits = partner_styling.get("outfits")
|
||||
if isinstance(outfits, list):
|
||||
outfit_text = _human_join([_clean_text(item) for item in outfits if _clean_text(item)])
|
||||
outfit_text = _natural_label_text(outfit_text, labels)
|
||||
if outfit_text:
|
||||
parts.append(f"Softcore partner styling: {outfit_text}")
|
||||
pose = _clean_text(partner_styling.get("pose"))
|
||||
if pose:
|
||||
parts.append(f"The shared softcore cast pose is {pose}")
|
||||
if soft_text:
|
||||
parts.append(f"Softcore version: {soft_text}")
|
||||
if hard_text:
|
||||
parts.append(f"Hardcore version: {hard_text}")
|
||||
if not parts:
|
||||
return None
|
||||
return _join_sentences(parts), "metadata(insta_of_pair)"
|
||||
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(
|
||||
_caption_metadata_route_request(row, detail_level, keep_style, target),
|
||||
_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 (
|
||||
_insta_of_pair_from_row,
|
||||
_configured_cast_from_row,
|
||||
@@ -630,10 +267,12 @@ def _metadata_to_prose(row: dict[str, Any], detail_level: str, keep_style: bool)
|
||||
_couple_from_row,
|
||||
_group_or_layout_from_row,
|
||||
):
|
||||
result = builder(row, detail_level, keep_style)
|
||||
result = builder(row, detail_level, keep_style, target)
|
||||
if result:
|
||||
return result
|
||||
return _text_to_prose(_clean_text(row.get("caption") or row.get("prompt")), detail_level, keep_style)
|
||||
prose, method = result
|
||||
return _append_formatter_hints(prose, row), method
|
||||
prose, method = _text_to_prose(_clean_text(row.get("caption") or row.get("prompt")), detail_level, keep_style)
|
||||
return _append_formatter_hints(prose, row), method
|
||||
|
||||
|
||||
def _prompt_to_prose(text: str, detail_level: str, keep_style: bool) -> tuple[str, str] | None:
|
||||
@@ -713,6 +352,23 @@ def _text_to_prose(text: str, detail_level: str, keep_style: bool) -> tuple[str,
|
||||
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(
|
||||
source_text: str,
|
||||
metadata_json: str = "",
|
||||
@@ -721,16 +377,50 @@ def naturalize_caption(
|
||||
include_trigger: bool = True,
|
||||
detail_level: str = "balanced",
|
||||
style_policy: str = "drop_style_tail",
|
||||
caption_profile: str = caption_policy.CAPTION_PROFILE_DEFAULT,
|
||||
target: str = "auto",
|
||||
) -> tuple[str, str]:
|
||||
"""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"
|
||||
detail_level = detail_level if detail_level in ("concise", "balanced", "dense") else "balanced"
|
||||
keep_style = style_policy == "keep_style_terms"
|
||||
row, row_method = _row_from_inputs(source_text, metadata_json, input_hint)
|
||||
if row is not None:
|
||||
prose, method = _metadata_to_prose(row, detail_level, keep_style)
|
||||
caption = sanitize_prose_text(_with_trigger(prose, trigger, include_trigger), triggers=(trigger,))
|
||||
return caption, f"{row_method}:{method}"
|
||||
prose, method = _text_to_prose(source_text, detail_level, keep_style)
|
||||
caption = sanitize_prose_text(_with_trigger(prose, trigger, include_trigger), triggers=(trigger,))
|
||||
return caption, method
|
||||
return caption_format_route.naturalize_caption(
|
||||
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(),
|
||||
)
|
||||
|
||||
|
||||
def naturalize_caption_with_trace(
|
||||
source_text: str,
|
||||
metadata_json: str = "",
|
||||
input_hint: str = "auto",
|
||||
target: str = "auto",
|
||||
trigger: str = DEFAULT_TRIGGER,
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from . import formatter_detail as detail_policy
|
||||
from . import formatter_input as input_policy
|
||||
from . import route_metadata as route_metadata_policy
|
||||
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 route_metadata as route_metadata_policy
|
||||
|
||||
|
||||
OLD_TRIGGER = "sxcpinup_coloredpencil"
|
||||
DEFAULT_TRIGGER = "sxcppnl7"
|
||||
|
||||
DETAIL_LEVELS = detail_policy.DETAIL_LEVELS
|
||||
STYLE_POLICIES = ("drop_style_tail", "keep_style_terms")
|
||||
CAPTION_PROFILE_DEFAULT = "manual_controls"
|
||||
|
||||
CAPTION_PROFILES = {
|
||||
"manual_controls": {},
|
||||
"training_concise": {
|
||||
"detail_level": "concise",
|
||||
"style_policy": "drop_style_tail",
|
||||
"include_trigger": True,
|
||||
},
|
||||
"training_dense": {
|
||||
"detail_level": "dense",
|
||||
"style_policy": "drop_style_tail",
|
||||
"include_trigger": True,
|
||||
},
|
||||
"browsing": {
|
||||
"detail_level": "balanced",
|
||||
"style_policy": "keep_style_terms",
|
||||
"include_trigger": False,
|
||||
},
|
||||
}
|
||||
|
||||
STYLE_TAILS = [
|
||||
", coloured pencil comic illustration, crisp linework, hatching, soft pastel palette, warm sensual lighting, textured parchment paper",
|
||||
", coloured pencil comic illustration, crisp linework, hatching, soft pastel palette, warm sensual lighting, textured paper",
|
||||
]
|
||||
|
||||
ITEM_LABELS = (
|
||||
"Sexual pose",
|
||||
"Erotic outfit",
|
||||
"Clothing",
|
||||
)
|
||||
|
||||
ACTION_FAMILY_CAPTION_LABELS = {
|
||||
"anal": "anal action",
|
||||
"foreplay": "foreplay action",
|
||||
"manual": "manual action",
|
||||
"outercourse": "non-penetrative action",
|
||||
"oral": "oral action",
|
||||
"penetration": "penetrative action",
|
||||
"threesome": "three-person action",
|
||||
"group": "group action",
|
||||
"toy_double": "toy-assisted double-contact action",
|
||||
"climax": "climax action",
|
||||
}
|
||||
|
||||
POSITION_FAMILY_CAPTION_LABELS = {
|
||||
"penetrative": "penetrative action",
|
||||
"foreplay": "foreplay action",
|
||||
"interaction": "interaction beat",
|
||||
"manual": "manual action",
|
||||
"oral": "oral action",
|
||||
"outercourse": "non-penetrative action",
|
||||
"anal": "anal action",
|
||||
"climax": "climax action",
|
||||
"threesome": "three-person action",
|
||||
"group": "group action",
|
||||
}
|
||||
|
||||
|
||||
def normalize_detail_level(value: str) -> str:
|
||||
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:
|
||||
value = _choice_key(value)
|
||||
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]:
|
||||
return list(CAPTION_PROFILES)
|
||||
|
||||
|
||||
def normalize_caption_profile(value: str) -> str:
|
||||
value = _choice_key(value)
|
||||
return value if value in CAPTION_PROFILES else CAPTION_PROFILE_DEFAULT
|
||||
|
||||
|
||||
def apply_caption_profile(
|
||||
caption_profile: str,
|
||||
*,
|
||||
detail_level: str,
|
||||
style_policy: str,
|
||||
include_trigger: bool,
|
||||
) -> tuple[str, str, bool]:
|
||||
profile = CAPTION_PROFILES[normalize_caption_profile(caption_profile)]
|
||||
return (
|
||||
normalize_detail_level(profile.get("detail_level", detail_level)),
|
||||
normalize_style_policy(profile.get("style_policy", style_policy)),
|
||||
bool(profile.get("include_trigger", include_trigger)),
|
||||
)
|
||||
|
||||
|
||||
def keep_style_terms(style_policy: str) -> bool:
|
||||
return normalize_style_policy(style_policy) == "keep_style_terms"
|
||||
|
||||
|
||||
def detail_allows(level: str, dense_only: bool = False) -> bool:
|
||||
return detail_policy.detail_allows(level, dense_only=dense_only)
|
||||
|
||||
|
||||
def strip_style_tail(text: str) -> str:
|
||||
text = input_policy.clean_text(text)
|
||||
for tail in STYLE_TAILS:
|
||||
if text.endswith(tail):
|
||||
return text[: -len(tail)].strip(" ,")
|
||||
return text
|
||||
|
||||
|
||||
def metadata_action_label(row: dict[str, Any], default: str = "sexual pose") -> str:
|
||||
position_family = route_metadata_policy.row_position_family(row)
|
||||
if position_family in POSITION_FAMILY_CAPTION_LABELS:
|
||||
return POSITION_FAMILY_CAPTION_LABELS[position_family]
|
||||
action_family = route_metadata_policy.row_action_family(row)
|
||||
if action_family in ACTION_FAMILY_CAPTION_LABELS:
|
||||
return ACTION_FAMILY_CAPTION_LABELS[action_family]
|
||||
return default
|
||||
|
||||
|
||||
def normalize_composition(text: str) -> str:
|
||||
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:
|
||||
text = input_policy.clean_text(text)
|
||||
text = re.sub(r",?\s*fashion editorial styling$", "", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r",?\s*resort styling$", "", text, flags=re.IGNORECASE)
|
||||
return text.strip(" ,")
|
||||
@@ -0,0 +1,319 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Callable
|
||||
|
||||
try:
|
||||
from . import caption_metadata_routes
|
||||
from . import caption_policy
|
||||
from . import formatter_input as input_policy
|
||||
from . import item_axis_policy
|
||||
from . import krea_cast as cast_policy
|
||||
from . import route_metadata as route_metadata_policy
|
||||
from . import softcore_text_policy
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
import caption_metadata_routes
|
||||
import caption_policy
|
||||
import formatter_input as input_policy
|
||||
import item_axis_policy
|
||||
import krea_cast as cast_policy
|
||||
import route_metadata as route_metadata_policy
|
||||
import softcore_text_policy
|
||||
|
||||
|
||||
OLD_TRIGGER = caption_policy.OLD_TRIGGER
|
||||
DEFAULT_TRIGGER = caption_policy.DEFAULT_TRIGGER
|
||||
PROMPT_FIELD_LABELS = input_policy.prompt_field_labels()
|
||||
ITEM_LABELS = caption_policy.ITEM_LABELS
|
||||
|
||||
|
||||
def clean_text(value: Any) -> str:
|
||||
return input_policy.clean_text(value)
|
||||
|
||||
|
||||
def is_false(value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value is False
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ("false", "0", "no", "off")
|
||||
return False
|
||||
|
||||
|
||||
def expression_disabled(row: dict[str, Any]) -> bool:
|
||||
return bool(row.get("expression_disabled")) or is_false(row.get("expression_enabled", True))
|
||||
|
||||
|
||||
def cap_first(text: str) -> str:
|
||||
text = clean_text(text).strip(" ,")
|
||||
return text[:1].upper() + text[1:] if text else ""
|
||||
|
||||
|
||||
def article(noun_phrase: str) -> str:
|
||||
word = noun_phrase.lstrip().lower()
|
||||
if word.startswith("hour") or word[:1] in "aeiou":
|
||||
return "an"
|
||||
return "a"
|
||||
|
||||
|
||||
def sentence(text: str) -> str:
|
||||
text = clean_text(text).strip(" ,;")
|
||||
if not text:
|
||||
return ""
|
||||
if text[-1] not in ".!?":
|
||||
text += "."
|
||||
return cap_first(text)
|
||||
|
||||
|
||||
def join_sentences(parts: list[str]) -> str:
|
||||
return " ".join(part for part in (sentence(part) for part in parts) if part)
|
||||
|
||||
|
||||
def formatter_hint_parts(row: dict[str, Any]) -> list[str]:
|
||||
hints: list[str] = []
|
||||
if not isinstance(row, dict):
|
||||
return hints
|
||||
for hint in route_metadata_policy.row_formatter_hints(row, "caption"):
|
||||
hint = clean_text(hint).strip(" .")
|
||||
if hint and hint not in hints:
|
||||
hints.append(hint)
|
||||
return hints
|
||||
|
||||
|
||||
def append_formatter_hints(prose: str, row: dict[str, Any]) -> str:
|
||||
hints = formatter_hint_parts(row)
|
||||
if not hints:
|
||||
return prose
|
||||
return join_sentences([prose, *hints])
|
||||
|
||||
|
||||
def human_join(parts: list[str]) -> str:
|
||||
parts = [part for part in (clean_text(part) for part in parts) if part]
|
||||
if len(parts) <= 1:
|
||||
return "".join(parts)
|
||||
if len(parts) == 2:
|
||||
return f"{parts[0]} and {parts[1]}"
|
||||
return f"{', '.join(parts[:-1])}, and {parts[-1]}"
|
||||
|
||||
|
||||
def metadata_action_label(row: dict[str, Any], default: str = "sexual pose") -> str:
|
||||
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:
|
||||
return cast_policy.prompt_cast_descriptors(text)
|
||||
|
||||
|
||||
def cast_entries(text: str) -> list[tuple[str, str]]:
|
||||
return cast_policy.cast_entries(text)
|
||||
|
||||
|
||||
def natural_cast_descriptor_text(text: str) -> str:
|
||||
return cast_policy.natural_cast_descriptor_text(text)
|
||||
|
||||
|
||||
def cast_labels(text: str) -> list[str]:
|
||||
return cast_policy.cast_labels(text)
|
||||
|
||||
|
||||
def natural_label_text(text: Any, labels: list[str]) -> str:
|
||||
return cast_policy.natural_label_text(text, labels, capitalize_sentence_starts=False)
|
||||
|
||||
|
||||
def strip_style_tail(text: str) -> str:
|
||||
return caption_policy.strip_style_tail(text)
|
||||
|
||||
|
||||
def remove_trigger(text: str, trigger: str) -> str:
|
||||
return input_policy.strip_trigger_prefix(
|
||||
text,
|
||||
(trigger, OLD_TRIGGER, DEFAULT_TRIGGER),
|
||||
remove_exact=True,
|
||||
)
|
||||
|
||||
|
||||
def with_trigger(text: str, trigger: str, include_trigger: bool) -> str:
|
||||
text = join_sentences([text]) if "." not in text else clean_text(text)
|
||||
trigger = clean_text(trigger or DEFAULT_TRIGGER)
|
||||
if not include_trigger or not trigger:
|
||||
return text
|
||||
if text.lower().startswith(trigger.lower() + "."):
|
||||
return text
|
||||
return f"{trigger}. {text}"
|
||||
|
||||
|
||||
def prompt_field(text: str, label: str) -> str:
|
||||
return input_policy.prompt_field(text, label, field_labels=PROMPT_FIELD_LABELS)
|
||||
|
||||
|
||||
def row_value(row: dict[str, Any], key: str, labels: tuple[str, ...] = ()) -> str:
|
||||
return input_policy.row_value(row, key, labels, field_labels=PROMPT_FIELD_LABELS)
|
||||
|
||||
|
||||
def field_row_value(row: dict[str, Any], key: str) -> str:
|
||||
return row_value(row, key)
|
||||
|
||||
|
||||
def field_from_any_prompt(text: str, labels: tuple[str, ...]) -> str:
|
||||
for label in labels:
|
||||
value = input_policy.prompt_field(text, label, field_labels=PROMPT_FIELD_LABELS)
|
||||
if value:
|
||||
return value
|
||||
return ""
|
||||
|
||||
|
||||
def normalize_composition(text: str) -> str:
|
||||
return caption_policy.normalize_composition(text)
|
||||
|
||||
|
||||
def clean_clothing(text: str) -> str:
|
||||
return caption_policy.clean_clothing(text)
|
||||
|
||||
|
||||
def body_phrase(body: Any, figure_note: Any = "") -> str:
|
||||
body = clean_text(body)
|
||||
figure_note = clean_text(figure_note)
|
||||
if not body:
|
||||
return figure_note
|
||||
if not figure_note:
|
||||
return f"{body} figure"
|
||||
if "figure" in figure_note.lower():
|
||||
return f"{body} build and {figure_note}"
|
||||
return f"{body} figure with {figure_note}"
|
||||
|
||||
|
||||
def single_caption_front(row: dict[str, Any]) -> dict[str, str]:
|
||||
caption = clean_text(row.get("caption"))
|
||||
if not caption:
|
||||
return {}
|
||||
caption = remove_trigger(strip_style_tail(caption), clean_text(row.get("trigger")) or DEFAULT_TRIGGER)
|
||||
caption = remove_trigger(caption, OLD_TRIGGER)
|
||||
subject = clean_text(row.get("primary_subject"))
|
||||
age = clean_text(row.get("age_band") or row.get("age"))
|
||||
phrase = clean_text(row.get("body_phrase"))
|
||||
if not phrase:
|
||||
body = clean_text(row.get("body_type") or row.get("body"))
|
||||
figure = clean_text(row.get("figure"))
|
||||
phrase = body_phrase(body, figure)
|
||||
front = f"{subject}, {age}, {phrase}, "
|
||||
if subject in ("woman", "man") and age and phrase and caption.startswith(front):
|
||||
try:
|
||||
skin, hair, eyes, _rest = caption[len(front) :].split(", ", 3)
|
||||
except ValueError:
|
||||
return {}
|
||||
else:
|
||||
pieces = [piece.strip() for piece in caption.split(", ", 6)]
|
||||
if len(pieces) < 7:
|
||||
return {}
|
||||
subject, age, phrase, skin, hair, eyes, _rest = pieces
|
||||
if subject not in ("woman", "man"):
|
||||
return {}
|
||||
return {
|
||||
"caption_subject": subject,
|
||||
"caption_age": age,
|
||||
"caption_body_phrase": phrase,
|
||||
"caption_skin": skin,
|
||||
"caption_hair": hair,
|
||||
"caption_eyes": eyes,
|
||||
}
|
||||
|
||||
|
||||
def pose_clause(pose: str) -> str:
|
||||
pose = clean_text(pose)
|
||||
if not pose:
|
||||
return ""
|
||||
first = pose.split(None, 1)[0].lower()
|
||||
if first.endswith("ing") or first in ("seated", "reclined", "posed"):
|
||||
return pose
|
||||
return f"posing in {pose}"
|
||||
|
||||
|
||||
def age_subject(age: str, subject: str) -> str:
|
||||
age = clean_text(age)
|
||||
subject = clean_text(subject) or "person"
|
||||
if not age:
|
||||
return f"An adult {subject}"
|
||||
clean_age = re.sub(r"\s+adults?$", "", age).strip()
|
||||
if "year-old" in clean_age:
|
||||
return f"A {clean_age} adult {subject}"
|
||||
if re.search(r"\d", clean_age):
|
||||
poss = "her" if subject == "woman" else "his"
|
||||
return f"An adult {subject} in {poss} {clean_age}"
|
||||
return f"An adult {clean_age} {subject}"
|
||||
|
||||
|
||||
def clean_age_phrase(age: str) -> str:
|
||||
age = clean_text(age)
|
||||
age = re.sub(r"\s+adults?$", "", age).strip()
|
||||
return age.replace("-year-old", " years old")
|
||||
|
||||
|
||||
def subject_phrase_from_counts(row: dict[str, Any]) -> str:
|
||||
subject = clean_text(row.get("subject_phrase"))
|
||||
if subject:
|
||||
return subject
|
||||
try:
|
||||
women = int(row.get("women_count") or 0)
|
||||
men = int(row.get("men_count") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return clean_text(row.get("primary_subject")) or "adult scene"
|
||||
parts = []
|
||||
if women:
|
||||
parts.append(f"{women} adult {'woman' if women == 1 else 'women'}")
|
||||
if men:
|
||||
parts.append(f"{men} adult {'man' if men == 1 else 'men'}")
|
||||
if not parts:
|
||||
return clean_text(row.get("primary_subject")) or "adult scene"
|
||||
return " and ".join(parts)
|
||||
|
||||
|
||||
def verb_for_row(row: dict[str, Any]) -> str:
|
||||
try:
|
||||
return "is" if int(row.get("person_count") or 0) == 1 else "are"
|
||||
except (TypeError, ValueError):
|
||||
return "are"
|
||||
|
||||
|
||||
def detail_allows(level: str, dense_only: bool = False) -> bool:
|
||||
return caption_policy.detail_allows(level, dense_only=dense_only)
|
||||
|
||||
|
||||
def metadata_route_dependencies(
|
||||
metadata_to_prose: Callable[..., tuple[str, str]],
|
||||
) -> caption_metadata_routes.CaptionMetadataRouteDependencies:
|
||||
return caption_metadata_routes.CaptionMetadataRouteDependencies(
|
||||
item_labels=ITEM_LABELS,
|
||||
clean_text=clean_text,
|
||||
row_value=row_value,
|
||||
field_row_value=field_row_value,
|
||||
clean_clothing=clean_clothing,
|
||||
normalize_composition=normalize_composition,
|
||||
expression_disabled=expression_disabled,
|
||||
detail_allows=detail_allows,
|
||||
join_sentences=join_sentences,
|
||||
human_join=human_join,
|
||||
article=article,
|
||||
cap_first=cap_first,
|
||||
body_phrase=body_phrase,
|
||||
single_caption_front=single_caption_front,
|
||||
pose_clause=pose_clause,
|
||||
age_subject=age_subject,
|
||||
clean_age_phrase=clean_age_phrase,
|
||||
subject_phrase_from_counts=subject_phrase_from_counts,
|
||||
verb_for_row=verb_for_row,
|
||||
metadata_action_label=metadata_action_label,
|
||||
item_axis_detail_text=item_axis_detail_text,
|
||||
natural_cast_descriptor_text=natural_cast_descriptor_text,
|
||||
cast_labels=cast_labels,
|
||||
natural_label_text=natural_label_text,
|
||||
softcore_caption_setup_phrase=softcore_text_policy.softcore_caption_setup_phrase,
|
||||
metadata_to_prose=metadata_to_prose,
|
||||
)
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
try:
|
||||
from . import character_config as character_policy
|
||||
except ImportError: # Allows local smoke tests with top-level imports.
|
||||
import character_config as character_policy
|
||||
|
||||
|
||||
Choose = Callable[[Any, list[tuple[str, str, str]]], tuple[str, str, str]]
|
||||
|
||||
|
||||
def count_phrase(count: int, singular: str, plural: str) -> str:
|
||||
words = {
|
||||
0: "no",
|
||||
1: "one",
|
||||
2: "two",
|
||||
3: "three",
|
||||
4: "four",
|
||||
5: "five",
|
||||
6: "six",
|
||||
7: "seven",
|
||||
8: "eight",
|
||||
9: "nine",
|
||||
10: "ten",
|
||||
11: "eleven",
|
||||
12: "twelve",
|
||||
}
|
||||
label = singular if count == 1 else plural
|
||||
return f"{words.get(count, str(count))} {label}"
|
||||
|
||||
|
||||
def cast_summary_phrase(women_count: int, men_count: int) -> str:
|
||||
women_count = max(0, int(women_count))
|
||||
men_count = max(0, int(men_count))
|
||||
if women_count + men_count == 0:
|
||||
women_count = 1
|
||||
person_count = women_count + men_count
|
||||
women_label = "woman" if women_count == 1 else "women"
|
||||
men_label = "man" if men_count == 1 else "men"
|
||||
return f"{women_count} {women_label}, {men_count} {men_label}, {person_count} total adults"
|
||||
|
||||
|
||||
def explicit_character_slot_label(slot: dict[str, Any]) -> str:
|
||||
label = str(slot.get("label") or "").strip().upper()
|
||||
if label in character_policy.CHARACTER_LABEL_CHOICES and label != "AUTO_CHAIN":
|
||||
return label
|
||||
return ""
|
||||
|
||||
|
||||
def character_slot_label_map(slots: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
||||
label_map: dict[str, dict[str, Any]] = {}
|
||||
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
for subject_type, prefix in (("woman", "Woman"), ("man", "Man")):
|
||||
subject_slots = [slot for slot in slots if slot.get("subject_type") == subject_type]
|
||||
auto_slots = [slot for slot in subject_slots if not explicit_character_slot_label(slot)]
|
||||
for index, slot in enumerate(reversed(auto_slots)):
|
||||
if index >= len(letters):
|
||||
break
|
||||
label_map[f"{prefix} {letters[index]}"] = slot
|
||||
for slot in subject_slots:
|
||||
explicit = explicit_character_slot_label(slot)
|
||||
if explicit:
|
||||
label_map[f"{prefix} {explicit}"] = slot
|
||||
return label_map
|
||||
|
||||
|
||||
def configured_cast_context(women_count: int, men_count: int) -> dict[str, str]:
|
||||
women_count = max(0, int(women_count))
|
||||
men_count = max(0, int(men_count))
|
||||
if women_count + men_count == 0:
|
||||
women_count = 1
|
||||
parts = []
|
||||
if women_count:
|
||||
parts.append(count_phrase(women_count, "adult woman", "adult women"))
|
||||
if men_count:
|
||||
parts.append(count_phrase(men_count, "adult man", "adult men"))
|
||||
subject_phrase = parts[0] if len(parts) == 1 else f"{parts[0]} and {parts[1]}"
|
||||
person_count = women_count + men_count
|
||||
if person_count == 1:
|
||||
scene_kind = "solo adult sexual pose"
|
||||
elif person_count == 2:
|
||||
scene_kind = "adult couple sex scene"
|
||||
elif person_count == 3:
|
||||
scene_kind = "adult threesome sex scene"
|
||||
else:
|
||||
scene_kind = "adult group sex scene"
|
||||
return {
|
||||
"subject_type": "configured_cast",
|
||||
"subject": f"{women_count}w_{men_count}m_sex_scene",
|
||||
"subject_phrase": subject_phrase,
|
||||
"age": "21+ adults",
|
||||
"body": "varied",
|
||||
"skin": "",
|
||||
"hair": "",
|
||||
"eyes": "",
|
||||
"body_phrase": "varied adult bodies",
|
||||
"women_count": str(women_count),
|
||||
"men_count": str(men_count),
|
||||
"person_count": str(person_count),
|
||||
"cast_summary": cast_summary_phrase(women_count, men_count),
|
||||
"scene_kind": scene_kind,
|
||||
}
|
||||
|
||||
|
||||
def couple_type_from_counts(
|
||||
rng: Any,
|
||||
women_count: int,
|
||||
men_count: int,
|
||||
*,
|
||||
choose: Choose,
|
||||
couple_types: list[tuple[str, str, str]],
|
||||
) -> tuple[str, str, str, int, int]:
|
||||
women_count = max(0, int(women_count))
|
||||
men_count = max(0, int(men_count))
|
||||
if women_count >= 2 and men_count == 0:
|
||||
return "two women", "two women", "close affectionate couple pose", 2, 0
|
||||
if men_count >= 2 and women_count == 0:
|
||||
return "two men", "two men", "relaxed romantic couple pose", 0, 2
|
||||
if women_count >= 1 and men_count >= 1:
|
||||
return "woman and man", "a woman and a man", "playful date-night pose", 1, 1
|
||||
|
||||
primary_subject, subject_phrase, pose = choose(rng, couple_types)
|
||||
if primary_subject == "two women":
|
||||
return primary_subject, subject_phrase, pose, 2, 0
|
||||
if primary_subject == "two men":
|
||||
return primary_subject, subject_phrase, pose, 0, 2
|
||||
return primary_subject, subject_phrase, pose, 1, 1
|
||||
@@ -7,8 +7,8 @@
|
||||
"weight": 1.0,
|
||||
"subject_type": "woman",
|
||||
"item_label": "Clothing",
|
||||
"style": "tasteful adult fashion-editorial coloured-pencil comic illustration with casual everyday styling",
|
||||
"positive_suffix": "Use crisp clean comic linework, soft fabric texture, detailed hatching, warm natural light, and tactile textured paper.",
|
||||
"style": "realistic casual social-feed photo with everyday styling",
|
||||
"positive_suffix": "Complete outfit visibility, clear fabric texture, natural light, coherent anatomy, and polished casual detail.",
|
||||
"expression_pools": ["casual_observational_expressions"],
|
||||
"composition_pools": ["casual_fashion_compositions"],
|
||||
"subcategories": [
|
||||
@@ -833,8 +833,8 @@
|
||||
"weight": 1.0,
|
||||
"subject_type": "man",
|
||||
"item_label": "Clothing",
|
||||
"style": "tasteful adult menswear fashion-editorial coloured-pencil comic illustration with casual everyday styling",
|
||||
"positive_suffix": "Use crisp clean comic linework, structured fabric texture, detailed hatching, natural light, and tactile textured paper.",
|
||||
"style": "realistic casual menswear social-feed photo with everyday styling",
|
||||
"positive_suffix": "Complete outfit visibility, structured fabric texture, natural light, coherent anatomy, and polished casual detail.",
|
||||
"expression_pools": ["men_casual_expressions"],
|
||||
"composition_pools": ["men_casual_compositions"],
|
||||
"subcategories": [
|
||||
@@ -1285,8 +1285,8 @@
|
||||
"weight": 1.0,
|
||||
"subject_type": "couple",
|
||||
"item_label": "Clothing",
|
||||
"style": "tasteful adult couple fashion-editorial coloured-pencil comic illustration with coordinated casual styling",
|
||||
"positive_suffix": "Use crisp clean comic linework, readable full outfits, detailed hatching, warm natural light, and tactile textured paper.",
|
||||
"style": "realistic casual couple social-feed photo with coordinated styling",
|
||||
"positive_suffix": "Complete coordinated outfits, clear fabric texture, warm natural light, coherent body placement, and polished casual detail.",
|
||||
"expression_pools": ["couple_casual_expressions"],
|
||||
"composition_pools": ["couple_casual_compositions"],
|
||||
"subcategories": [
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"weight": 1.0,
|
||||
"subject_type": "woman",
|
||||
"item_label": "Erotic outfit",
|
||||
"style": "explicit adult erotic fashion illustration, sensual pin-up coloured-pencil comic style, adults only",
|
||||
"positive_suffix": "Use crisp clean comic linework, detailed hatching, soft skin shading, tactile fabric texture, warm intimate lighting, and textured paper.",
|
||||
"style": "explicit adult erotic fashion scene with sensual pin-up styling, adults only",
|
||||
"positive_suffix": "Use clear adult anatomy, readable erotic outfit construction, tactile fabric texture, warm intimate lighting, coherent body placement, and polished detail.",
|
||||
"negative_prompt": "minors, childlike appearance, schoolgirl, childlike costume, non-consensual, coercion, violence, injury, watermark",
|
||||
"scene_pools": ["softcore_creator_scenes", "mirror_scenes"],
|
||||
"expression_pools": ["softcore_creator_expressions", "erotic_inviting_expressions"],
|
||||
|
||||
@@ -0,0 +1,802 @@
|
||||
{
|
||||
"version": 1,
|
||||
"atlas_root": "/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2",
|
||||
"purpose": "Machine-readable Krea2 POV pose-geometry catalog for fixed-seed SxCP prompt tuning.",
|
||||
"status_values": {
|
||||
"proven": "A route has atlas support and repeated or structural Krea2 evidence strong enough for generator defaults.",
|
||||
"candidate": "A route has atlas support but needs more fixed-seed Krea2 tests before changing generator defaults.",
|
||||
"unstable": "A route has known text-only limits and should prefer control images or a narrower variant."
|
||||
},
|
||||
"variants": [
|
||||
{
|
||||
"key": "pov_doggy_top_down_rear_entry",
|
||||
"family": "doggy",
|
||||
"status": "proven",
|
||||
"atlas_folders": ["doggy", "doggy_alt"],
|
||||
"action_family": "penetration",
|
||||
"position_keys": ["doggy", "rear_entry", "on_all_fours"],
|
||||
"canonical_geometry": "Top-down first-person rear-entry view from behind: viewer body cues at the bottom, hands near the woman's hips, woman on all fours with chest low, forearms folded, cheek turned sideways far ahead, back arched, and hips raised toward the camera.",
|
||||
"prompt_cues": [
|
||||
"top-down POV doggy position from behind",
|
||||
"camera looks down over the viewer's hands onto the woman's raised hips",
|
||||
"woman is on all fours with chest low, forearms folded, cheek turned sideways",
|
||||
"back arched, hips raised high toward the camera",
|
||||
"natural lower-body POV cues in the foreground"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"visible shoes or lower legs as the standing cue",
|
||||
"viewer torso and thighs outside frame",
|
||||
"face or mouth as the fluid target for rear-entry climax"
|
||||
],
|
||||
"reference_images": [
|
||||
"doggy/65_doggy.png",
|
||||
"doggy_alt/100_doggy_alt.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["doggy", "all fours", "rear-entry"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["65", "52", "5202"],
|
||||
"guide_section": "docs/krea2-prompt-guide.md#pov-doggy--rear-entry",
|
||||
"notes": "Visible viewer thighs, torso, or pelvis can be correct; shoes/lower-leg wording caused oral drift."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_boobjob_upright_cleavage",
|
||||
"family": "boobjob",
|
||||
"status": "proven",
|
||||
"atlas_folders": ["boobjob"],
|
||||
"action_family": "outercourse",
|
||||
"position_keys": ["boobjob", "titjob", "breast_sex"],
|
||||
"canonical_geometry": "Frontal upright first-person view: viewer reclines with thighs open while the woman faces him between his legs, breasts pressed together around a vertical shaft, glans above the cleavage near her mouth.",
|
||||
"prompt_cues": [
|
||||
"POV boobjob position",
|
||||
"woman kneels upright between his legs facing him",
|
||||
"penis rises vertically in the lower foreground",
|
||||
"squeezed between her pressed-together breasts",
|
||||
"woman's own fingers and nails cup her breasts from the outside",
|
||||
"glans emerging above the cleavage directly below her mouth"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"torso bent forward over his pelvis",
|
||||
"both hands push her breasts without naming whose hands",
|
||||
"only foreground hands when the woman's hands are the intended hands"
|
||||
],
|
||||
"reference_images": [
|
||||
"boobjob/100_boobjob.png",
|
||||
"boobjob/18_boobjob.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["boobjob", "titjob", "breast sex"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["7301", "7302"],
|
||||
"guide_section": "docs/krea2-prompt-guide.md#boobjob--titjob",
|
||||
"notes": "Same-seed A/B showed upright cleavage-sleeve wording improves contact pressure; hand ownership must be explicit."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_handjob_upright_centered",
|
||||
"family": "handjob",
|
||||
"status": "proven",
|
||||
"atlas_folders": ["handjob"],
|
||||
"action_family": "outercourse",
|
||||
"position_keys": ["handjob"],
|
||||
"canonical_geometry": "Centered first-person view: viewer reclines with thighs open, the woman faces him between his legs, and the woman's hand is the main contact anchor on the shaft with her face and torso behind it.",
|
||||
"prompt_cues": [
|
||||
"POV handjob position",
|
||||
"woman kneels between his legs facing him",
|
||||
"the woman's right hand wraps around the viewer's penis",
|
||||
"her left hand steadies the base",
|
||||
"viewer thighs and pelvis frame the lower edges",
|
||||
"without his hands covering the action"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"generic one hand grips when hand ownership matters",
|
||||
"foreground hands competing with the woman's active hand"
|
||||
],
|
||||
"reference_images": [
|
||||
"handjob/18_handjob.png",
|
||||
"handjob/92_handjob.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["handjob", "hand job", "hand stroking"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["7401"],
|
||||
"guide_section": "docs/krea2-prompt-guide.md#handjob",
|
||||
"notes": "Same-seed A/B showed explicit woman-hand ownership removed viewer-hand ambiguity."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_ballsucking_low_head",
|
||||
"family": "ballsucking",
|
||||
"status": "candidate",
|
||||
"atlas_folders": ["ballsucking"],
|
||||
"action_family": "outercourse",
|
||||
"position_keys": ["testicle_sucking", "ballsucking"],
|
||||
"canonical_geometry": "Low first-person pelvis view: the woman stays low beside or between the viewer's open thighs, with cheek/thigh proximity, the scrotum as the mouth surface, scrotal skin as the nearest mouth surface, and testicles resting across her open lips while both testicles rest against her tongue from below as the accepted partial target.",
|
||||
"prompt_cues": [
|
||||
"woman bends forward and kneels very low between the viewer's open thighs",
|
||||
"chest low over the viewer's pelvis",
|
||||
"low side-pelvis POV",
|
||||
"face is the closest visible partner part",
|
||||
"cheek against the viewer's inner thigh",
|
||||
"scrotum is the mouth surface",
|
||||
"scrotal skin is the nearest mouth surface",
|
||||
"testicles resting across her open lips while her tongue cups them from below",
|
||||
"both testicles rest against her tongue from below",
|
||||
"viewer abdomen and inner thighs frame the close foreground"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"head tucked under the penis shaft without testicle-height wording",
|
||||
"repeating shaft/hand-on-shaft wording before scrotum/testicle contact is established",
|
||||
"viewer first as the main subject",
|
||||
"mid-height head placement"
|
||||
],
|
||||
"reference_images": [
|
||||
"ballsucking/101_ballsucking.png",
|
||||
"ballsucking/4_ballsucking.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["testicle_sucking", "balls licking", "testicle"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["238365845574312", "1212121212", "5757575757", "6262626262", "9797979797", "9898989898", "5959595959", "6060606060", "6161616161", "7171717171", "7272727272"],
|
||||
"guide_section": "docs/krea2-prompt-guide.md#ballsucking--testicle-sucking",
|
||||
"notes": "Fifty-probe threshold search accepted tongue/lips on testicles as a partial improvement over baseline shaft/glans collapse; generator carried the side-low partial axis provisionally. Fresh seed 6262626262 then showed open-lips scrotum-surface wording on turns 252 and 258 improved target contact over the generated-route controls 250 and 256. Fresh seed 9797979797 repeated the scrotal-skin target-object branch on turns 288 and 293, with scrotal skin as the nearest mouth surface and both testicles resting against tongue from below. Fresh seed 9898989898 validated the patched generated route on turns 296 and 297, preserving side-low cheek/thigh geometry while keeping scrotum/testicles at the tongue/lip contact. Fresh seed 5959595959 tested lip-oval, sideways mouth pocket, and chin-pelvis upward seal wording across three women; all branches kept some low-pelvis geometry but collapsed back toward shaft/glans contact, so record it as a weak case. Fresh seed 6060606060 tested foreground occlusion, under-scrotum tongue shelf, and hand-guided scrotum wording; every branch still became shaft-centered or hand/shaft-dominant, so keep the route candidate and do not patch those axes. Fresh seed 6161616161 tested exact mouth-sucking, single-testicle, hanging-balls-below-shaft, side-mouth-wrap, and chin-pelvis lower-mouth wording across three women; generated-route controls stayed the best repeated partials on turns 331 and 337, side-mouth and chin-pelvis branches produced isolated useful partials on turns 335 and 348, and the rest collapsed back to shaft/glans contact. Fresh seed 7171717171 tested flat pelvis-valley, thigh-tunnel, pubic-hair mouth-line, low-cushion chin-anchor, and pelvis-edge target-first wording across three women; flat pelvis-valley repeated a better viewer-flat body plane on turns 350, 356, and 362 but stayed shaft-centered, while the cushion and pelvis-edge branches drifted into wrong open-thigh/presentation geometry. Fresh seed 7272727272 tested hybrid flat-valley scrotal-skin, valley-floor open-lips, upper-frame shaft lower-scrotum, cropped upper-shaft valley-mouth, and side-low flat-valley wording; the flat-valley branch repeated the body plane on turns 368, 374, and 380 but stayed shaft-centered, and side-low flat-valley gave only look hints. Stop text-only expansion for now: do not patch those hybrid axes. The provisional generator route uses scrotum-as-mouth-surface, testicles resting across open lips, and scrotal-skin nearest-surface wording while staying candidate."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_footjob_frontal_sole_stroke",
|
||||
"family": "footjob",
|
||||
"status": "proven",
|
||||
"atlas_folders": ["footjob"],
|
||||
"action_family": "outercourse",
|
||||
"position_keys": ["footjob"],
|
||||
"canonical_geometry": "Frontal first-person footjob view: viewer reclines with thighs framing the lower foreground while the woman sits opposite with two large overlapping soles dominating the lower center foreground, inner arches pressing inward around the upright shaft, toes curled around both edges, a narrow visible strip of shaft and glans rising between the compressed feet, and her body and face behind the feet.",
|
||||
"prompt_cues": [
|
||||
"POV footjob position",
|
||||
"viewer reclines with thighs framing the lower foreground",
|
||||
"woman sits opposite facing him with legs open toward the camera",
|
||||
"two large overlapping soles dominate the lower center foreground",
|
||||
"inner arches press inward from both sides around the upright shaft",
|
||||
"toes curl around both edges",
|
||||
"narrow visible strip of shaft and glans rises between the compressed feet",
|
||||
"woman's face and torso stay visible behind the large foreground feet"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"generic foot contact without both soles around the shaft",
|
||||
"hands replacing the feet as the main contact",
|
||||
"mouth or hand action competing with the footjob",
|
||||
"feet off to the side without centered penis contact"
|
||||
],
|
||||
"reference_images": [
|
||||
"footjob/59_footjob.png",
|
||||
"footjob/86_footjob.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["footjob", "foot job", "feet stroking"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["238365845574312", "3434343434", "6868686868", "7373737373"],
|
||||
"guide_section": "docs/krea2-prompt-guide.md#footjob",
|
||||
"notes": "Same-seed two-woman expansions repeated the two-sole clamp as a provisional generator improvement over valid baselines; seed 6868686868 showed overlapping soles plus a narrow visible shaft/glans strip is more reliable than generic large-sole wording. Fresh seed 7373737373 then repeated the generated overlapping-sole/narrow-strip route across two women on turns 264 and 267, with tight center-gap repeats on turns 265 and 268. Promote the default route to proven and keep cross-foot side press as an alternate branch."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_fingering_reclined_open_thighs",
|
||||
"family": "fingering",
|
||||
"status": "candidate",
|
||||
"atlas_folders": ["fingering"],
|
||||
"action_family": "manual",
|
||||
"position_keys": ["fingering", "open_thighs"],
|
||||
"canonical_geometry": "First-person manual-contact view: the woman reclines or sits back with thighs spread wide toward the camera, face and torso visible behind the open legs, and the viewer hand enters from the foreground to make the visible contact between her legs.",
|
||||
"prompt_cues": [
|
||||
"POV fingering position",
|
||||
"woman reclines with thighs spread wide toward the camera",
|
||||
"viewer hand enters from the foreground",
|
||||
"fingers make the central contact between her open thighs",
|
||||
"her face and torso remain visible behind the open-leg frame",
|
||||
"thighs and knees form the main framing around the action"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"generic hand near the body without visible manual contact",
|
||||
"the woman's own hand replacing the POV hand",
|
||||
"mouth, foot, or penetration action competing with the foreground hand",
|
||||
"closed legs hiding the contact point"
|
||||
],
|
||||
"reference_images": [
|
||||
"fingering/103_fingering.png",
|
||||
"fingering/69_fingering.png",
|
||||
"fingering/80_fingering.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["fingering", "finger", "manual stimulation"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": [],
|
||||
"guide_section": "",
|
||||
"notes": "Atlas shows a repeated open-thigh manual-contact POV layout; needs fixed-seed Krea2 tests before promotion to proven."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_wand_foreground_tool_contact",
|
||||
"family": "wand",
|
||||
"status": "proven",
|
||||
"atlas_folders": ["wand"],
|
||||
"action_family": "toy",
|
||||
"position_keys": ["wand", "toy_contact", "open_thighs"],
|
||||
"canonical_geometry": "First-person toy-contact view: the woman reclines or sits back with thighs spread toward the camera, face and torso visible behind the open-leg frame, and the viewer hand holds a single continuous teal wand-style massager from the foreground with the rounded bulb head pressed flat to the central contact point.",
|
||||
"prompt_cues": [
|
||||
"POV wand toy position",
|
||||
"woman reclines with thighs spread wide toward the camera",
|
||||
"single continuous teal wand-style massager is the largest lower-frame object",
|
||||
"viewer hand holds a wand-style toy from the foreground",
|
||||
"rounded bulb head presses flat to her vulva and clit as the central contact point",
|
||||
"her face and torso remain visible behind the open-leg frame",
|
||||
"thighs and knees form the main frame around the foreground tool"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"generic toy nearby without contact",
|
||||
"the woman holding the toy when the foreground viewer hand is intended",
|
||||
"mouth, foot, or penetration action competing with the toy contact",
|
||||
"closed legs hiding the contact point",
|
||||
"toy floating without a visible hand or handle"
|
||||
],
|
||||
"reference_images": [
|
||||
"wand/106_wand_.png",
|
||||
"wand/107_wand_.png",
|
||||
"wand/108_wand_.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["wand", "toy", "vibrator"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["246813579", "8642086420", "7979797979"],
|
||||
"guide_section": "docs/krea2-prompt-guide.md#wand-toy-contact",
|
||||
"notes": "The teal lower-right single-continuous-wand axis repeated across two women on sampler seeds 8642086420 and 7979797979 and validated through generated-route turns 197, 234, and 238. The pale upper-left wand remains a useful alternate branch; oversized bulb wording can hide contact."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_ejaculation_aftermath_open_thigh_candidate",
|
||||
"family": "ready",
|
||||
"status": "candidate",
|
||||
"atlas_folders": ["ready"],
|
||||
"action_family": "climax",
|
||||
"position_keys": ["open_thighs", "camera_showing"],
|
||||
"canonical_geometry": "First-person post-ejaculation open-thigh display: the woman reclines facing the viewer with thighs spread open, face and torso readable behind the open-leg frame, viewer lower-body cue near the lower edge, and thick semen plus clear fluid visibly coating and dripping around the exposed pussy and anal opening.",
|
||||
"prompt_cues": [
|
||||
"POV post-ejaculation open-thigh display pose",
|
||||
"woman reclines facing the viewer with thighs spread open",
|
||||
"thick semen and clear fluid are visible around the exposed pussy and anal opening",
|
||||
"the body stays still after ejaculation",
|
||||
"viewer lower-body cue stays near the lower edge",
|
||||
"her face and torso remain visible behind the open-leg frame",
|
||||
"thighs and knees frame the wet aftermath detail without hiding it"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"generic ready/setup pose before sex",
|
||||
"active thrusting or penetration-in-progress wording",
|
||||
"turning the setup into oral, toy, or manual contact",
|
||||
"generic wetness without thick visible fluid around the exposed opening",
|
||||
"closed thighs hiding the aftermath detail",
|
||||
"cropping out the face and torso behind the open-leg frame"
|
||||
],
|
||||
"reference_images": [
|
||||
"ready/105_ready_.png",
|
||||
"ready/106_ready_.png",
|
||||
"ready/107_ready_.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["post-ejaculation open-thigh display", "thick visible semen or fluid", "open thighs"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["1123581321"],
|
||||
"guide_section": "docs/krea2-prompt-guide.md#ready--post-ejaculation-open-thigh-display",
|
||||
"notes": "The ready folder is a post-ejaculation open-thigh display pose with thick visible fluid around the exposed opening, not a neutral ready/setup pose. First fixed-seed evidence on source 52 was mirrored into the generator as a provisional improvement; repeat before promotion to proven."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_spread_open_thigh_presentation",
|
||||
"family": "spread",
|
||||
"status": "candidate",
|
||||
"atlas_folders": ["spread"],
|
||||
"action_family": "interaction",
|
||||
"position_keys": ["open_thighs", "camera_showing"],
|
||||
"canonical_geometry": "Frontal open-thigh presentation view: the woman faces the camera with legs raised and knees held wide, thighs forming a wide V-frame toward the viewer, face and torso visible behind the open-leg pose, and no required partner contact.",
|
||||
"prompt_cues": [
|
||||
"POV open-thigh presentation position",
|
||||
"woman faces the camera with legs raised and knees held wide",
|
||||
"thighs form a wide V-frame toward the viewer",
|
||||
"face and torso remain visible behind the open-leg pose",
|
||||
"hands hold the knees open",
|
||||
"no partner contact is required for this setup pose"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"adding penetration or manual contact by default",
|
||||
"closed thighs hiding the open-leg geometry",
|
||||
"cropping out the face and torso behind the leg frame",
|
||||
"turning the pose into doggy or side-lying geometry"
|
||||
],
|
||||
"reference_images": [
|
||||
"spread/100_spread_.png",
|
||||
"spread/4_spread_.png",
|
||||
"spread/69_spread_.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["spread", "open thighs", "legs spread"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["3141592653"],
|
||||
"guide_section": "docs/krea2-prompt-guide.md#spread--open-thigh-presentation",
|
||||
"notes": "Same-seed A/B on source 50 and 47 showed raised-knee V-frame and hand-on-knee hierarchy improves over generic spread wording. Mirrored into the generator as a provisional improvement; repeat on another seed before promotion to proven."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_sixty_nine_close_reversed_oral",
|
||||
"family": "sixty_nine",
|
||||
"status": "unstable",
|
||||
"difficulty": "hardest",
|
||||
"priority": "low",
|
||||
"control_requirement": "pose_or_image_guidance_first",
|
||||
"atlas_folders": ["69"],
|
||||
"action_family": "oral",
|
||||
"position_keys": ["sixty_nine"],
|
||||
"canonical_geometry": "Close first-person sixty-nine view: the visible partner is reversed over the viewer with hips closest to the camera, head and torso receding away into the upper frame, viewer mouth anchoring the lower foreground, and hands holding the hips to make the reversed body arrangement readable.",
|
||||
"prompt_cues": [
|
||||
"POV close sixty-nine position",
|
||||
"visible partner is reversed over the viewer with hips closest to the camera",
|
||||
"the partner's head and torso recede away into the upper frame",
|
||||
"viewer mouth anchors the lower foreground under the partner's hips",
|
||||
"viewer hands hold the partner's hips without changing the reversed-over-viewer body arrangement",
|
||||
"mutual oral geometry stays readable as one continuous first-person frame"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"side-by-side sixty-nine layout",
|
||||
"upright oral pose with the partner facing the viewer",
|
||||
"generic oral contact without the reversed-over-viewer body arrangement",
|
||||
"cropping away the head-and-torso direction that proves the sixty-nine setup",
|
||||
"text-only prompting when exact geometry matters; prefer pose control or image guidance"
|
||||
],
|
||||
"reference_images": [
|
||||
"69/105_sixtynine.png",
|
||||
"69/106_sixtynine.png",
|
||||
"69/50_sixtynine.png",
|
||||
"69/80_sixtynine.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["sixty-nine", "reversed over viewer", "mutual oral"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": [],
|
||||
"guide_section": "",
|
||||
"notes": "Lowest-priority atlas route for now: geometry is consistent but visually fragile for text-only Krea2 prompting. Treat it as a pose/control-image or image-guidance-first case, not a normal prompt-only fixed-seed candidate."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_blowjob_top_down_vertical_shaft",
|
||||
"family": "blowjob_top_view",
|
||||
"status": "candidate",
|
||||
"atlas_folders": ["blowjob_top_view"],
|
||||
"action_family": "oral",
|
||||
"position_keys": ["kneeling", "top_down_oral"],
|
||||
"canonical_geometry": "Nadir-angle standing male POV top-view oral view: the viewer looks almost straight down from his torso toward the floor, nearby floor plane dominates the image, the viewer abdomen, shorts, thighs, and feet frame the lower foreground, the shaft is a short centered vertical column, and the woman kneels directly below between his feet with hair crown, forehead, shoulders, hands, knees, mouth, and shaft alignment visible from above.",
|
||||
"prompt_cues": [
|
||||
"nadir-angle standing male POV top-view oral position",
|
||||
"viewer looks almost straight down from his torso toward the floor",
|
||||
"nearby carpet/floor plane dominates the image",
|
||||
"viewer abdomen, shorts, thighs, and feet frame the lower foreground",
|
||||
"shaft is a short centered vertical column",
|
||||
"the woman kneels directly below the viewer between his feet",
|
||||
"her mouth seals around the centered shaft",
|
||||
"one hand wraps the base",
|
||||
"hair crown, forehead, shoulders, hands, and knees are visible from above",
|
||||
"desk legs, chair wheels, carpet texture, and floor seams act as top-down office anchors"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"side-view oral framing",
|
||||
"woman standing level with the viewer",
|
||||
"shaft angled sideways or cropped away from the mouth",
|
||||
"hands replacing the mouth as the main oral contact",
|
||||
"camera placed behind the woman instead of above the viewer",
|
||||
"literal plumb-line or map wording that renders as drawn graphics"
|
||||
],
|
||||
"reference_images": [
|
||||
"blowjob_top_view/22_blowjob_top_view.png",
|
||||
"blowjob_top_view/27_blowjob_top_view.png",
|
||||
"blowjob_top_view/102_blowjob_top_view.png",
|
||||
"blowjob_top_view/2_blowjob_top_view.png",
|
||||
"blowjob_top_view/85_blowjob_top_view.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["kneeling oral", "top-down oral", "oral"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["4242424242"],
|
||||
"guide_section": "docs/krea2-prompt-guide.md#blowjob-top-view--overhead-vertical-shaft",
|
||||
"notes": "Same-sampler source 46/47 A/B showed that top-down oral hierarchy tightens hand-at-base support and centered shaft-to-mouth alignment over generic kneeling oral. A follow-up axis loop on the same seed showed that generic steep-overhead wording can still feel horizontal, while nadir-angle standing male POV plus a dominating nearby floor plane, the woman directly between the viewer's feet, top-down office anchors, and a short centered vertical shaft column gives the strongest atlas-like verticality. Avoid plumb-line/map wording because Krea2 can literalize it as drawn graphics. Keep candidate until another source or seed repeats the nadir-angle axis."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_blowjob_side_profile_oral",
|
||||
"family": "blowjob_side",
|
||||
"status": "proven",
|
||||
"atlas_folders": ["blowjob_side"],
|
||||
"action_family": "oral",
|
||||
"position_keys": ["side_lying", "reclining_oral", "penis_licking"],
|
||||
"canonical_geometry": "Side-profile first-person oral body-line view: the male viewer's abdomen, navel, pelvis, and near thigh create the broad lower-frame foreground surface, the adult male viewer's own torso starts at the lower edge and runs diagonally into the lower-right foreground, the woman enters laterally from the left edge beside his hip, and her side-facing mouth plus hand contact align to the shaft at the male abdomen line.",
|
||||
"prompt_cues": [
|
||||
"POV side-profile oral body-line position",
|
||||
"male viewer's abdomen, navel, pelvis, and near thigh create a broad horizontal body surface",
|
||||
"adult male viewer's own torso starts at the lower edge and runs diagonally into the lower-right foreground",
|
||||
"navel, abdomen hair, pelvis, and near thigh mark the camera owner's body",
|
||||
"woman enters laterally from the left edge beside his hip",
|
||||
"cheek and jaw stay in profile",
|
||||
"mouth on the shaft at the male abdomen line",
|
||||
"lips touching the shaft at the male abdomen line",
|
||||
"mouth-to-shaft contact is the nearest facial detail",
|
||||
"hand around the base under her lips",
|
||||
"shoulder and torso trail sideways along the edge"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"top-down oral framing",
|
||||
"front-facing centered face instead of side profile",
|
||||
"woman standing level with the viewer",
|
||||
"camera behind the woman",
|
||||
"hands replacing the mouth as the main oral contact",
|
||||
"pure male-body-axis wording that exposes the male as a photographed subject",
|
||||
"transferring the central body surface to the woman"
|
||||
],
|
||||
"reference_images": [
|
||||
"blowjob_side/103_blowjob_side.png",
|
||||
"blowjob_side/105_blowjob_side.png",
|
||||
"blowjob_side/29_blowjob_side.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["side_lying", "side-lying oral", "blowjob_side", "side-profile oral", "oral"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["5656565656", "9753197531", "9595959595", "9696969696", "5858585858"],
|
||||
"guide_section": "docs/krea2-prompt-guide.md#blowjob-side-profile--side-phone-weak-case",
|
||||
"notes": "Seed 5656565656 first produced attractive side-phone / external side-camera oral compositions across source 46 and 47, but not valid POV evidence. A later source-46 candidate with explicit adult-male foreground ownership recovered a more atlas-like first-person body-line view, while a related source-47 body-axis candidate failed by transferring the central body surface to the woman. Seed 9753197531 then repeated the lateral-edge body-line wording across two women. Generated-route turn 207 showed the route also needs lips-touching and mouth-to-shaft-contact priority to keep the mouth from floating above the shaft. Seed 9595959595 repeated the lower-right torso anchor on turns 279 and 283 across two visible women, improving camera-owner torso ownership over a control that could expose the male as a photographed side subject. Seed 9696969696 generated-route validation repeated the patched route on turns 284 and 285, keeping lower-right own-body foreground, profile mouth contact, and office depth across two visible women. Seed 5858585858 added a three-woman generated-route repeat on turns 298, 301, and 304; all controls preserved the patched camera-owner lower-right body plane, lateral profile entry, mouth contact at the abdomen line, and office depth. Promote the generated side-profile POV hierarchy to proven while keeping side-camera-style self-body crop wording as a look branch rather than the default."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_blowjob_laying_frontal_oral",
|
||||
"family": "blowjob_laying",
|
||||
"status": "candidate",
|
||||
"atlas_folders": ["blowjob_laying"],
|
||||
"action_family": "oral",
|
||||
"position_keys": ["reclining_oral", "penis_licking"],
|
||||
"canonical_geometry": "Frontal prone first-person oral view: the viewer reclines with open thighs framing the lower foreground, the woman lies belly-down between the viewer's open thighs, and her front-facing mouth and hands align to a shaft rising from the lower center of the frame.",
|
||||
"prompt_cues": [
|
||||
"POV prone frontal oral position",
|
||||
"viewer reclines with open thighs framing the lower foreground",
|
||||
"woman lies belly-down between the viewer's open thighs",
|
||||
"her chest and shoulders stay low over the viewer's pelvis",
|
||||
"shaft rises from the lower center toward her front-facing mouth",
|
||||
"her hands grip and steady the shaft while mouth contact remains the main action"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"side-profile oral framing",
|
||||
"top-down oral framing from above the viewer",
|
||||
"woman kneeling upright instead of lying forward",
|
||||
"woman standing level with the viewer",
|
||||
"hands replacing the mouth as the main oral contact"
|
||||
],
|
||||
"reference_images": [
|
||||
"blowjob_laying/101_blowjob_laying.png",
|
||||
"blowjob_laying/103_blowjob_laying.png",
|
||||
"blowjob_laying/60_blowjob_laying.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["blowjob_laying", "prone frontal oral", "oral"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["6767676767"],
|
||||
"guide_section": "docs/krea2-prompt-guide.md#blowjob-laying-frontal--wide-v-frame",
|
||||
"notes": "Seed 6767676767 improved source 46 and 50 with a wide symmetrical V-frame, lower abdomen near-edge anchor, torso stretched low and horizontal between the viewer's thighs, hands at the base, and centered mouth-to-shaft contact. Baselines were already strong but read more raised-hips or all-fours than prone belly-down, so keep the route candidate until another seed repeats the low-horizontal body improvement."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_blowjob_sitting_upright_oral",
|
||||
"family": "blowjob_sitting",
|
||||
"status": "candidate",
|
||||
"atlas_folders": ["blowjob_sitting"],
|
||||
"action_family": "oral",
|
||||
"position_keys": ["reclining_oral", "penis_licking", "blowjob_sitting"],
|
||||
"canonical_geometry": "Upright seated first-person oral view: the viewer reclines with open thighs framing the lower foreground, the woman sits upright between the viewer's open thighs, and her close front-facing mouth aligns to a vertical shaft centered between the viewer's legs.",
|
||||
"prompt_cues": [
|
||||
"POV upright sitting oral position",
|
||||
"viewer reclines with open thighs framing the lower foreground",
|
||||
"woman sits low between the viewer's open thighs with torso upright behind the action",
|
||||
"her face lowers close to the exact center contact point",
|
||||
"vertical shaft centered between the viewer's legs",
|
||||
"her open mouth covers the centered tip with hands wrapped low at the base"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"prone belly-down oral framing",
|
||||
"side-profile oral framing",
|
||||
"top-down oral framing from above the viewer",
|
||||
"woman standing level with the viewer",
|
||||
"cropping away the viewer's open-thigh frame"
|
||||
],
|
||||
"reference_images": [
|
||||
"blowjob_sitting/100_blowjob_sitting.png",
|
||||
"blowjob_sitting/24_blowjob_sitting.png",
|
||||
"blowjob_sitting/58_blowjob_sitting.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["blowjob_sitting", "upright sitting oral", "oral"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["7878787878"],
|
||||
"guide_section": "docs/krea2-prompt-guide.md#blowjob-sitting-upright--low-mouth-contact",
|
||||
"notes": "Seed 7878787878 improved source 46 and 50 with low-mouth seated hierarchy: viewer thigh V-frame, lower abdomen near edge, woman sitting low between the thighs with torso upright behind the action, face lowered to the exact center contact point, open mouth covering the centered shaft tip, and both hands wrapped at the base. Source 50 had some outfit looseness/drift, so keep the route candidate and provisional until another seed repeats it."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_missionary_open_leg_penetration",
|
||||
"family": "missionary",
|
||||
"status": "candidate",
|
||||
"atlas_folders": ["missionary"],
|
||||
"action_family": "penetration",
|
||||
"position_keys": ["missionary", "open_thighs", "front_entry"],
|
||||
"canonical_geometry": "First-person missionary view from above the viewer's pelvis: the woman reclines on her back facing the viewer, knees open toward the viewer, thighs frame the central contact line, and the viewer's lower body and hands anchor the lower foreground.",
|
||||
"prompt_cues": [
|
||||
"POV missionary open-leg penetration position",
|
||||
"woman reclines on her back with knees open toward the viewer",
|
||||
"her face, torso, and open thighs remain visible in one frame",
|
||||
"viewer is positioned between her legs from the lower foreground",
|
||||
"thighs frame the central penetration line",
|
||||
"viewer hands hold her thighs without blocking the contact geometry",
|
||||
"flat elevated-support examples place the viewer braced at the foot edge with feet and shins below the support edge"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"folded-leg or knees-to-chest geometry",
|
||||
"rear-entry or doggy geometry",
|
||||
"side-profile framing",
|
||||
"cropping away the woman's face and torso",
|
||||
"viewer standing far back instead of positioned between her legs"
|
||||
],
|
||||
"reference_images": [
|
||||
"missionary/101_missionary.png",
|
||||
"missionary/102_missionary.png",
|
||||
"missionary/1_missionary.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["missionary", "open-leg penetration", "front-entry"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["8989898989"],
|
||||
"guide_section": "missionary-open-leg--seated-lounge-drift",
|
||||
"notes": "Same-seed batches on 8989898989 show two valid subcases. Generic/angled missionary can preserve open thighs, viewer hands, and centered contact, while the flatter atlas examples need elevated-support edge placement: woman flat across a table/platform, viewer standing or braced at the foot edge, and viewer feet/shins or side-dropping legs below the support. The accepted turn84 axis is mirrored only into the raised-edge/edge-supported route as a provisional patch; keep generic missionary available and keep the catalog candidate until another seed repeats it."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_missionary_folded_high_leg_penetration",
|
||||
"family": "missionary_folded",
|
||||
"status": "candidate",
|
||||
"atlas_folders": ["missionary_folded"],
|
||||
"action_family": "penetration",
|
||||
"position_keys": ["missionary_folded", "front_entry"],
|
||||
"canonical_geometry": "First-person folded missionary view from above the viewer's pelvis: the woman reclines on her back facing the viewer, knees folded high toward her chest, feet and ankles close to the camera, and the viewer's hands hold her calves and ankles while the central contact line stays below the raised legs.",
|
||||
"prompt_cues": [
|
||||
"POV folded missionary high-leg penetration position",
|
||||
"woman reclines on her back with knees folded high toward her chest",
|
||||
"viewer lower abdomen and a large centered shaft anchor the lower center first",
|
||||
"compact folded-knee block sits above the contact point",
|
||||
"feet and ankles sit close to the camera above the contact line",
|
||||
"viewer hands hold her calves and ankles in the foreground",
|
||||
"her face and torso remain visible behind the raised legs",
|
||||
"central penetration line stays below the folded knees"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"normal open-leg missionary with knees spread low",
|
||||
"rear-entry or doggy geometry",
|
||||
"side-profile framing",
|
||||
"legs cropped away or relaxed flat on the bed",
|
||||
"feet replacing the penetration geometry as the main action"
|
||||
],
|
||||
"reference_images": [
|
||||
"missionary_folded/16_missionary_folded.png",
|
||||
"missionary_folded/50_missionary_folded.png",
|
||||
"missionary_folded/80_missionary_folded.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["missionary_folded", "folded missionary", "knees-to-chest"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["8989898989"],
|
||||
"guide_section": "missionary-folded--contact-first-knee-block",
|
||||
"notes": "Same-seed turns 85-92 show that subject-first knees-to-chest wording can produce folded high-leg geometry, but Krea2 drops readable shaft/contact when the knees and feet dominate first. The accepted turn89 axis puts the viewer lower abdomen and large centered shaft/contact before the compact folded-knee block, then holds calves/ankles and keeps the face/torso behind the raised knees. Mirrored into the folded-missionary route as a provisional patch; keep catalog candidate until another seed or subject repeats it."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_cowgirl_frontal_straddle_penetration",
|
||||
"family": "cowgirl",
|
||||
"status": "proven",
|
||||
"atlas_folders": ["5.cowgirl"],
|
||||
"action_family": "penetration",
|
||||
"position_keys": ["cowgirl", "frontal_straddle", "woman_on_top"],
|
||||
"canonical_geometry": "First-person frontal cowgirl view: the viewer reclines below while the woman straddles the viewer facing him, knees open on both sides, torso upright above the contact line, and the viewer's thighs and pelvis anchor the lower foreground.",
|
||||
"prompt_cues": [
|
||||
"POV frontal cowgirl straddle penetration position",
|
||||
"woman straddles the viewer facing him",
|
||||
"her torso stays upright above the viewer",
|
||||
"viewer lower abdomen and pelvis anchor the bottom edge",
|
||||
"wide horizontal thigh bridge spans left edge to right edge",
|
||||
"her knees are open on both sides of the viewer's hips",
|
||||
"viewer reclines below with thighs and pelvis in the lower foreground",
|
||||
"viewer hands hold her thighs without blocking the centered contact line"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"missionary with the woman lying on her back",
|
||||
"reverse cowgirl with the woman facing away",
|
||||
"folded-leg knees-to-chest geometry",
|
||||
"rear-entry or doggy geometry",
|
||||
"cropping away the upright torso and straddling knees"
|
||||
],
|
||||
"reference_images": [
|
||||
"5.cowgirl/100_cowgirl.png",
|
||||
"5.cowgirl/101_cowgirl.png",
|
||||
"5.cowgirl/1_cowgirl.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["cowgirl", "frontal cowgirl", "woman-on-top"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["8989898989", "2828282828", "9191919191"],
|
||||
"guide_section": "cowgirl-frontal--wide-thigh-bridge",
|
||||
"notes": "Same-seed turns 93-96 show the generic baseline already validly hits frontal cowgirl on seed 8989898989. The best atlas-like improvement was turn95: wide horizontal thigh bridge from left edge to right edge, viewer lower abdomen/pelvis at the bottom edge, upright torso above the contact, and hands gripping the thigh sides. Seed 2828282828 then repeated the wide-thigh bridge hierarchy across two visible women on turns 209 and 213, and generated-route turn 216 validated the patched normal cowgirl route. Fresh seed 9191919191 repeated the generated route and three branch wordings across turns 242-249, with turns 242, 243, 244, and 248 giving the clearest atlas-like wide-thigh bridge. Promote the normal cowgirl route to proven while keeping cowgirl-alt and reverse-cowgirl families separate."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_cowgirl_alt_low_squat_penetration",
|
||||
"family": "cowgirl_alt",
|
||||
"status": "candidate",
|
||||
"atlas_folders": ["5.cowgirl_alt"],
|
||||
"action_family": "penetration",
|
||||
"position_keys": ["cowgirl_alt", "woman_on_top"],
|
||||
"canonical_geometry": "Close first-person cowgirl-alt view: the viewer lies flat on his back underneath while the woman faces him in a low seated squat over the viewer's pelvis, knees bent wide near the camera, torso close above the contact line, and ceiling plus upper-wall background cues confirm the low upward viewpoint.",
|
||||
"prompt_cues": [
|
||||
"POV low cowgirl seated-squat penetration position",
|
||||
"viewer lies flat on his back underneath her",
|
||||
"lens sits low at the viewer's abdomen looking upward from his pelvis",
|
||||
"woman faces the viewer in a low seated squat over the viewer's pelvis",
|
||||
"her knees are bent wide and close to the camera on both sides of the viewer's hips",
|
||||
"her torso stays close above the centered contact line",
|
||||
"ceiling lights and high partition lines appear behind her upper body",
|
||||
"viewer reclines below with thighs and lower torso anchoring the foreground",
|
||||
"viewer hands hold the underside of her thighs without blocking the centered contact line"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"upright distant cowgirl with the torso far from the viewer",
|
||||
"missionary with the woman lying on her back",
|
||||
"reverse cowgirl with the woman facing away",
|
||||
"folded-leg knees-to-chest geometry",
|
||||
"rear-entry or doggy geometry",
|
||||
"cropping away the wide bent knees and close seated position"
|
||||
],
|
||||
"reference_images": [
|
||||
"5.cowgirl_alt/101_cowgirl_alt.png",
|
||||
"5.cowgirl_alt/102_cowgirl_alt.png",
|
||||
"5.cowgirl_alt/103_cowgirl_alt.png",
|
||||
"5.cowgirl_alt/16_cowgirl_alt.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["cowgirl_alt", "low cowgirl", "seated-squat cowgirl", "woman-on-top"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["8989898989"],
|
||||
"guide_section": "cowgirl-alt--flat-supine-low-angle",
|
||||
"notes": "Same-seed turns 97-104 show that low-squat/contact wording can still miss the atlas orientation by reading as a platform/high-camera setup. The accepted turn104 axis uses flat-supine viewer wording plus ceiling and upper glass/room background cues: viewer abdomen and chest lie flat at the bottom, lens looks upward from his abdomen/pelvis, woman is mounted over him with wide bent knees, and centered contact remains readable. Mirrored into the cowgirl-alt route as a provisional patch; keep catalog candidate until another seed or subject repeats it."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_reverse_cowgirl_back_facing_penetration",
|
||||
"family": "reverse_cowgirl",
|
||||
"status": "candidate",
|
||||
"atlas_folders": ["cowgirl_reverse"],
|
||||
"action_family": "penetration",
|
||||
"position_keys": ["reverse_cowgirl", "back_facing_straddle", "woman_on_top"],
|
||||
"canonical_geometry": "First-person reverse cowgirl view: the viewer reclines below while the woman straddles the viewer facing away, her back and hips dominate the frame, her thighs sit on both sides of the viewer's hips, and the viewer's thighs and pelvis anchor the lower foreground.",
|
||||
"prompt_cues": [
|
||||
"POV reverse cowgirl back-facing penetration position",
|
||||
"woman faces away from the viewer in a back-facing straddle",
|
||||
"her back, hips, and ass dominate the nearest foreground",
|
||||
"her face turns over one shoulder without changing the back-facing pose",
|
||||
"her thighs are planted on both sides of the viewer's hips",
|
||||
"viewer lies underneath with thighs and pelvis anchoring the foreground",
|
||||
"viewer thighs frame the lower corners",
|
||||
"centered contact sits directly between her thighs below her ass",
|
||||
"viewer hands hold her hips without changing the woman-on-top geometry"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"frontal cowgirl with the woman facing the viewer",
|
||||
"missionary with the woman lying on her back",
|
||||
"rear-entry or doggy geometry with the viewer behind her",
|
||||
"woman on all fours",
|
||||
"side-profile penetration without the back-facing straddle",
|
||||
"cropping away the back, hips, and viewer-underneath foreground cues"
|
||||
],
|
||||
"reference_images": [
|
||||
"cowgirl_reverse/101_cowgirl_reverse.png",
|
||||
"cowgirl_reverse/104_cowgirl_reverse.png",
|
||||
"cowgirl_reverse/106_cowgirl_reverse.png",
|
||||
"cowgirl_reverse/1_cowgirl_reverse.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["cowgirl_reverse", "reverse cowgirl", "back-facing straddle", "woman-on-top"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["8989898989"],
|
||||
"guide_section": "reverse-cowgirl--close-back-hip-dominant",
|
||||
"notes": "Same-seed turns 105-108 show that generic facing-away reverse-cowgirl wording can collapse into frontal cowgirl. The accepted turn106 axis makes the back/hips/ass the nearest largest shapes, puts the viewer underneath with thighs framing the lower corners, and keeps centered contact directly between her thighs below her ass. Turns 107 and 108 are useful secondary evidence for viewer-leg V-frame and over-shoulder glance variants. Mirrored into the reverse-cowgirl route as a provisional patch; keep candidate until another seed or subject repeats it, and keep reverse-cowgirl-alt separate for the more upright seated atlas family."
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "pov_reverse_cowgirl_alt_upright_back_facing_penetration",
|
||||
"family": "reverse_cowgirl_alt",
|
||||
"status": "candidate",
|
||||
"atlas_folders": ["cowgirl_reversere_alt"],
|
||||
"action_family": "penetration",
|
||||
"position_keys": ["reverse_cowgirl_alt", "reverse_cowgirl", "back_facing_straddle", "woman_on_top", "upright_seated"],
|
||||
"canonical_geometry": "Upright first-person reverse cowgirl alt view: the viewer reclines below while the woman sits upright facing away in a back-facing straddle, her back and ass stay centered above the viewer's pelvis, her thighs frame the viewer's hips, and viewer hands hold her hips.",
|
||||
"prompt_cues": [
|
||||
"POV upright reverse cowgirl back-facing penetration position",
|
||||
"woman sits upright facing away from the viewer in a back-facing straddle",
|
||||
"her back stays vertical and readable above her hips",
|
||||
"her ass is centered above the viewer's pelvis while both thighs frame the viewer's hips",
|
||||
"viewer hands hold her hips",
|
||||
"viewer thighs frame the lower corners",
|
||||
"centered contact remains visible below her ass",
|
||||
"viewer reclines underneath with thighs and pelvis anchoring the foreground",
|
||||
"viewer hands hold her hips without changing the upright woman-on-top posture"
|
||||
],
|
||||
"avoid_cues": [
|
||||
"close hip-only reverse cowgirl crop without the upright back",
|
||||
"frontal cowgirl with the woman facing the viewer",
|
||||
"missionary with the woman lying on her back",
|
||||
"rear-entry or doggy geometry with the viewer behind her",
|
||||
"woman on all fours",
|
||||
"cropping away the vertical back and seated woman-on-top posture"
|
||||
],
|
||||
"reference_images": [
|
||||
"cowgirl_reversere_alt/100_cowgirl_reversere_alt.png",
|
||||
"cowgirl_reversere_alt/101_cowgirl_reversere_alt.png",
|
||||
"cowgirl_reversere_alt/102_cowgirl_reversere_alt.png",
|
||||
"cowgirl_reversere_alt/18_cowgirl_reversere_alt.png"
|
||||
],
|
||||
"generator_hook": {
|
||||
"module": "krea_pov_actions.py",
|
||||
"route_terms": ["cowgirl_reversere_alt", "reverse cowgirl alt", "upright back-facing straddle", "woman-on-top"]
|
||||
},
|
||||
"evidence": {
|
||||
"fixed_seed_tests": ["8989898989"],
|
||||
"guide_section": "reverse-cowgirl-alt--upright-seated-back-facing",
|
||||
"notes": "Same-seed turns 109-112 show that the upright seated reverse-cowgirl-alt family is distinct from the close normal reverse-cowgirl route. Turn 109's generic upright baseline was already valid, while turn110's vertical-back plus hands-on-hips wording best matched the alt atlas: back and shoulders stay upright/readable, ass centered over the viewer's pelvis, viewer hands hold both hips, viewer thighs frame the lower corners, and centered contact remains below her ass. Mirrored into a separate reverse-cowgirl-alt route as a provisional patch; keep candidate until another seed or subject repeats it."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+116
-36
@@ -7,14 +7,14 @@
|
||||
"weight": 1.0,
|
||||
"subject_type": "configured_cast",
|
||||
"item_label": "Sexual pose",
|
||||
"style": "explicit consensual adult hardcore sex illustration, anatomically clear erotic comic pin-up style, adults only",
|
||||
"positive_suffix": "Use clear adult anatomy, visible sexual contact, intense body language, crisp comic linework, detailed hatching, warm erotic lighting, and tactile textured paper.",
|
||||
"style": "explicit consensual adult hardcore sex scene, anatomically clear body positioning, all participants are adults",
|
||||
"positive_suffix": "Use clear adult anatomy, visible sexual contact, readable limb placement, precise body orientation, coherent spatial depth, and intense body language.",
|
||||
"negative_prompt": "minors, childlike appearance, teen, schoolgirl, incest, bestiality, non-consensual, coercion, rape, violence, injury, blood, gore, watermark",
|
||||
"scene_pools": ["hardcore_private_scenes"],
|
||||
"expression_pools": ["hardcore_orgasm_expressions", "hardcore_messy_expressions"],
|
||||
"composition_pools": ["hardcore_explicit_compositions"],
|
||||
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Sexual pose: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit, hardcore, and anatomically clear, with visible genital contact and adult bodies only. {positive_suffix} Avoid: {negative_prompt}.",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, {item}, {scene}, {composition}, explicit consensual adult hardcore sex illustration",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, {item}, {scene}, {composition}, explicit consensual adult hardcore sex scene",
|
||||
"expressions": [
|
||||
"adult ahegao-style orgasm face",
|
||||
"eyes rolled back with tongue out",
|
||||
@@ -92,17 +92,21 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 0.75,
|
||||
"item_template_metadata": {
|
||||
"action_family": "foreplay",
|
||||
"position_family": "foreplay"
|
||||
},
|
||||
"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, precise body orientation, and coherent spatial depth.",
|
||||
"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}.",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, foreplay teasing action: {item}, {scene}, {composition}, explicit consensual adult erotic foreplay illustration",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, foreplay teasing action: {item}, {scene}, {composition}, explicit consensual adult erotic foreplay scene",
|
||||
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||
"expression_pools": ["hardcore_foreplay_expressions"],
|
||||
"composition_pools": ["foreplay_compositions"],
|
||||
"item_templates": [
|
||||
"{tease_act} in {position}, with {touch_detail}, {clothing_detail}, and {mood_detail}",
|
||||
"{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}",
|
||||
"{position} while {tease_act}, with {face_detail}, {clothing_detail}, and {touch_detail}"
|
||||
],
|
||||
@@ -211,10 +215,14 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 0.85,
|
||||
"item_template_metadata": {
|
||||
"action_family": "manual",
|
||||
"position_family": "manual"
|
||||
},
|
||||
"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, precise hand placement, and coherent body orientation.",
|
||||
"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}.",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, manual stimulation action: {item}, {scene}, {composition}, explicit consensual adult manual stimulation illustration",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, manual stimulation action: {item}, {scene}, {composition}, explicit consensual adult manual stimulation scene",
|
||||
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||
"expression_pools": ["hardcore_manual_expressions"],
|
||||
"composition_pools": ["manual_stimulation_compositions"],
|
||||
@@ -222,7 +230,7 @@
|
||||
"{manual_act} in {position}, with {manual_detail}, {body_contact}, 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 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}"
|
||||
],
|
||||
"item_axes": {
|
||||
@@ -316,10 +324,14 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 0.7,
|
||||
"item_template_metadata": {
|
||||
"action_family": "foreplay",
|
||||
"position_family": "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, clear head position, precise limb placement, and coherent body orientation.",
|
||||
"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}.",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, body worship action: {item}, {scene}, {composition}, explicit consensual adult body-contact illustration",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, body worship action: {item}, {scene}, {composition}, explicit consensual adult body-contact scene",
|
||||
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||
"expression_pools": ["hardcore_interaction_expressions"],
|
||||
"composition_pools": ["interaction_compositions"],
|
||||
@@ -327,7 +339,7 @@
|
||||
"{worship_act} in {position}, with {touch_detail}, {face_detail}, and {visibility}",
|
||||
"{position} featuring {worship_act}, {body_contact}, {touch_detail}, and {reaction_detail}",
|
||||
"{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}"
|
||||
],
|
||||
"item_axes": {
|
||||
@@ -425,10 +437,14 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 0.65,
|
||||
"item_template_metadata": {
|
||||
"action_family": "foreplay",
|
||||
"position_family": "interaction"
|
||||
},
|
||||
"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, clear before-and-after body placement, and coherent spatial depth.",
|
||||
"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}.",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, clothing and position transition: {item}, {scene}, {composition}, explicit consensual adult transition illustration",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, clothing and position transition: {item}, {scene}, {composition}, explicit consensual adult transition scene",
|
||||
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||
"expression_pools": ["hardcore_interaction_expressions"],
|
||||
"composition_pools": ["interaction_compositions"],
|
||||
@@ -436,7 +452,7 @@
|
||||
"{transition_act} in {position}, with {clothing_detail}, {hand_detail}, and {visibility}",
|
||||
"{position} featuring {transition_act}, {body_contact}, {clothing_detail}, and {movement_detail}",
|
||||
"{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}"
|
||||
],
|
||||
"item_axes": {
|
||||
@@ -530,10 +546,14 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 0.55,
|
||||
"item_template_metadata": {
|
||||
"action_family": "foreplay",
|
||||
"position_family": "interaction"
|
||||
},
|
||||
"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, precise contact points, and coherent body orientation.",
|
||||
"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}.",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, dominant guidance action: {item}, {scene}, {composition}, explicit consensual adult power-dynamic illustration",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, dominant guidance action: {item}, {scene}, {composition}, explicit consensual adult power-dynamic scene",
|
||||
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||
"expression_pools": ["hardcore_interaction_expressions"],
|
||||
"composition_pools": ["interaction_compositions"],
|
||||
@@ -541,7 +561,7 @@
|
||||
"{control_act} in {position}, with {hand_detail}, {body_contact}, and {visibility}",
|
||||
"{position} featuring {control_act}, {power_detail}, {hand_detail}, and {reaction_detail}",
|
||||
"{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}"
|
||||
],
|
||||
"item_axes": {
|
||||
@@ -640,10 +660,14 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 0.6,
|
||||
"item_template_metadata": {
|
||||
"action_family": "foreplay",
|
||||
"position_family": "interaction"
|
||||
},
|
||||
"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, stable camera geography, and coherent subject framing.",
|
||||
"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}.",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, camera performance action: {item}, {scene}, {composition}, explicit consensual adult creator-performance illustration",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, camera performance action: {item}, {scene}, {composition}, explicit consensual adult creator-performance scene",
|
||||
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||
"expression_pools": ["hardcore_interaction_expressions"],
|
||||
"composition_pools": ["camera_performance_compositions"],
|
||||
@@ -651,7 +675,7 @@
|
||||
"{performance_act} in {position}, with {presentation_detail}, {hand_detail}, and {visibility}",
|
||||
"{position} featuring {performance_act}, {camera_detail}, {presentation_detail}, and {reaction_detail}",
|
||||
"{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}"
|
||||
],
|
||||
"item_axes": {
|
||||
@@ -744,10 +768,14 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 0.55,
|
||||
"item_template_metadata": {
|
||||
"action_family": "foreplay",
|
||||
"position_family": "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, precise role placement, and coherent spatial depth.",
|
||||
"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}.",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, group coordination action: {item}, {scene}, {composition}, explicit consensual adult group-interaction illustration",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, group coordination action: {item}, {scene}, {composition}, explicit consensual adult group-interaction scene",
|
||||
"scene_pools": ["hardcore_group_scenes", "hardcore_private_scenes"],
|
||||
"expression_pools": ["hardcore_group_expressions"],
|
||||
"composition_pools": ["group_coordination_compositions"],
|
||||
@@ -755,7 +783,7 @@
|
||||
"{coordination_act} with {arrangement}, {touch_detail}, {reaction_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}",
|
||||
"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}"
|
||||
],
|
||||
"item_axes": {
|
||||
@@ -846,10 +874,14 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 0.35,
|
||||
"item_template_metadata": {
|
||||
"action_family": "foreplay",
|
||||
"position_family": "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, clear body placement, and coherent spatial depth.",
|
||||
"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}.",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, aftercare and cleanup action: {item}, {scene}, {composition}, explicit consensual adult post-sex aftermath illustration",
|
||||
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, aftercare and cleanup action: {item}, {scene}, {composition}, explicit consensual adult post-sex aftermath scene",
|
||||
"scene_pools": ["hardcore_climax_scenes", "hardcore_bed_scenes", "hardcore_private_scenes"],
|
||||
"expression_pools": ["hardcore_aftercare_expressions"],
|
||||
"composition_pools": ["aftercare_compositions"],
|
||||
@@ -857,7 +889,7 @@
|
||||
"{aftercare_act} in {position}, with {cleanup_detail}, {body_contact}, and {visibility}",
|
||||
"{position} featuring {aftercare_act}, {touch_detail}, {cleanup_detail}, and {reaction_detail}",
|
||||
"{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}"
|
||||
],
|
||||
"item_axes": {
|
||||
@@ -950,11 +982,19 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 1.0,
|
||||
"item_template_metadata": {
|
||||
"action_family": "penetration",
|
||||
"position_family": "penetrative"
|
||||
},
|
||||
"scene_pools": ["hardcore_penetrative_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||
"expression_pools": ["hardcore_penetration_expressions"],
|
||||
"composition_pools": ["penetration_compositions"],
|
||||
"item_templates": [
|
||||
"{penetration_act} in {position}, with {body_contact}, {intensity}, and {visibility}",
|
||||
{
|
||||
"template": "{penetration_act} in {position}, with {body_contact}, {intensity}, and {visibility}",
|
||||
"action_family": "penetration",
|
||||
"position_family": "penetrative"
|
||||
},
|
||||
"{position} while {penetration_act}, {hand_detail}, {mouth_detail}, and {visibility}",
|
||||
"{penetration_act} from {angle}, with {leg_detail}, {body_contact}, and {intensity}",
|
||||
"hardcore {position} featuring {penetration_act}, {thrust_detail}, {hand_detail}, and {visibility}",
|
||||
@@ -1070,8 +1110,11 @@
|
||||
],
|
||||
"position": [
|
||||
"missionary position",
|
||||
"folded missionary position",
|
||||
"cowgirl position",
|
||||
"low cowgirl seated-squat position",
|
||||
"reverse cowgirl position",
|
||||
"upright reverse cowgirl position",
|
||||
"doggy style position",
|
||||
"standing sex position",
|
||||
"spooning sex position",
|
||||
@@ -1119,18 +1162,26 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 1.0,
|
||||
"item_template_metadata": {
|
||||
"action_family": "oral",
|
||||
"position_family": "oral"
|
||||
},
|
||||
"scene_pools": ["hardcore_oral_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||
"expression_pools": ["hardcore_oral_expressions"],
|
||||
"composition_pools": ["oral_compositions"],
|
||||
"item_templates": [
|
||||
"{oral_act} in {position}, with {hand_detail}, {expression_detail}, and {visibility}",
|
||||
{
|
||||
"template": "{oral_act} in {position}, with {hand_detail}, {expression_detail}, and {visibility}",
|
||||
"action_family": "oral",
|
||||
"position_family": "oral"
|
||||
},
|
||||
"{position} featuring {oral_act}, {body_contact}, {saliva_detail}, and {climax_hint}",
|
||||
"{oral_act} from {angle}, with {mouth_detail}, {hand_detail}, and {visibility}",
|
||||
"hardcore oral scene with {oral_act}, {body_contact}, {saliva_detail}, and {expression_detail}",
|
||||
"{oral_act} on {surface}, {hand_detail}, {mouth_detail}, and {climax_hint}",
|
||||
"{angle} view of {oral_act}, with {visibility}, {body_contact}, and {expression_detail}",
|
||||
"{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": {
|
||||
"angle": [
|
||||
@@ -1258,6 +1309,10 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 1.0,
|
||||
"item_template_metadata": {
|
||||
"action_family": "outercourse",
|
||||
"position_family": "outercourse"
|
||||
},
|
||||
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||
"expression_pools": ["hardcore_outercourse_expressions"],
|
||||
"compositions": [
|
||||
@@ -1271,10 +1326,14 @@
|
||||
{"text": "close candid creator-shot frame centered on non-penetrative genital contact", "min_people": 2, "max_people": 3}
|
||||
],
|
||||
"item_templates": [
|
||||
"{outer_act} in {position}, with {contact_detail}, {hand_detail}, and {visibility}",
|
||||
{
|
||||
"template": "{outer_act} in {position}, with {contact_detail}, {hand_detail}, and {visibility}",
|
||||
"action_family": "outercourse",
|
||||
"position_family": "outercourse"
|
||||
},
|
||||
"{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}",
|
||||
"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}",
|
||||
"{position} while {outer_act}, with {texture_detail}, {hand_detail}, and {visibility}"
|
||||
],
|
||||
@@ -1408,6 +1467,10 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 1.0,
|
||||
"item_template_metadata": {
|
||||
"action_family": "default",
|
||||
"position_family": "anal"
|
||||
},
|
||||
"scene_pools": ["hardcore_anal_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||
"expression_pools": ["hardcore_anal_dp_expressions"],
|
||||
"composition_pools": ["anal_dp_compositions"],
|
||||
@@ -1419,7 +1482,7 @@
|
||||
"{double_act} on {surface}, with {leg_detail}, {intensity}, and {climax_hint}",
|
||||
"{angle} view of {double_act}, {body_arrangement}, {mouth_detail}, and {visibility}",
|
||||
"{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": {
|
||||
"anal_act": [
|
||||
@@ -1627,6 +1690,10 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 1.0,
|
||||
"item_template_metadata": {
|
||||
"action_family": "default",
|
||||
"position_family": "threesome"
|
||||
},
|
||||
"scene_pools": ["hardcore_threesome_scenes", "hardcore_group_scenes", "hardcore_mirror_scenes"],
|
||||
"expression_pools": ["hardcore_group_expressions"],
|
||||
"composition_pools": ["threesome_compositions"],
|
||||
@@ -1634,7 +1701,7 @@
|
||||
"{threesome_act} with {body_arrangement}, {oral_detail}, {penetration_detail}, and {visibility}",
|
||||
"{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}",
|
||||
"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}",
|
||||
"{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}",
|
||||
@@ -1810,6 +1877,10 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 1.0,
|
||||
"item_template_metadata": {
|
||||
"action_family": "default",
|
||||
"position_family": "group"
|
||||
},
|
||||
"scene_pools": ["hardcore_group_scenes"],
|
||||
"expression_pools": ["hardcore_group_expressions"],
|
||||
"composition_pools": ["group_sex_compositions"],
|
||||
@@ -1817,7 +1888,7 @@
|
||||
"{group_act} with {arrangement}, {contact_detail}, {fluid_detail}, and {visibility}",
|
||||
"{arrangement} featuring {group_act}, {oral_detail}, {penetration_detail}, and {intensity}",
|
||||
"{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}",
|
||||
"{angle} view of {arrangement}, {fluid_detail}, {intensity}, and {climax_detail}",
|
||||
"explicit adult group pile with {group_act}, {oral_detail}, {penetration_detail}, and {visibility}",
|
||||
@@ -1982,11 +2053,19 @@
|
||||
"inherit_expressions": false,
|
||||
"inherit_compositions": false,
|
||||
"weight": 1.0,
|
||||
"item_template_metadata": {
|
||||
"action_family": "climax",
|
||||
"position_family": "climax"
|
||||
},
|
||||
"scene_pools": ["hardcore_climax_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||
"expression_pools": ["hardcore_climax_expressions"],
|
||||
"composition_pools": ["climax_compositions"],
|
||||
"item_templates": [
|
||||
"{climax_act} with {fluid_location}, {body_position}, {expression_detail}, and {visibility}",
|
||||
{
|
||||
"template": "{climax_act} with {fluid_location}, {body_position}, {expression_detail}, and {visibility}",
|
||||
"action_family": "climax",
|
||||
"position_family": "climax"
|
||||
},
|
||||
"{body_position} during {climax_act}, with {hand_detail}, {fluid_location}, and {fluid_detail}",
|
||||
"{angle} aftermath view with {body_position}, {body_contact}, and {visibility}",
|
||||
"hardcore post-ejaculation scene with {fluid_location}, {body_position}, {expression_detail}, and {visibility}",
|
||||
@@ -2029,6 +2108,7 @@
|
||||
"standing with cum on the body",
|
||||
"straddling a partner's hips in cowgirl position",
|
||||
"reverse cowgirl over a partner's hips",
|
||||
"upright reverse cowgirl over a partner's hips",
|
||||
"on all fours with hips raised",
|
||||
"face-down ass-up on the mattress",
|
||||
"side-lying with thighs parted",
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
RANDOM_SUBCATEGORY = "random"
|
||||
|
||||
CATEGORY_PRESETS = {
|
||||
"auto_weighted": ("auto_weighted", 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),
|
||||
"men_casual": ("Men casual clothes", RANDOM_SUBCATEGORY),
|
||||
"couple_casual": ("Couple casual clothes", RANDOM_SUBCATEGORY),
|
||||
"provocative_erotic": ("Provocative erotic clothes", RANDOM_SUBCATEGORY),
|
||||
"hardcore_pose": ("Hardcore sexual poses", RANDOM_SUBCATEGORY),
|
||||
"custom_random": ("custom_random", RANDOM_SUBCATEGORY),
|
||||
}
|
||||
|
||||
CAST_PRESETS = {
|
||||
"solo_woman": (1, 0),
|
||||
"solo_man": (0, 1),
|
||||
"mixed_couple": (1, 1),
|
||||
"two_women": (2, 0),
|
||||
"two_men": (0, 2),
|
||||
"threesome_2w1m": (2, 1),
|
||||
"small_group_3w2m": (3, 2),
|
||||
}
|
||||
|
||||
|
||||
def category_preset_choices() -> list[str]:
|
||||
return list(CATEGORY_PRESETS)
|
||||
|
||||
|
||||
def cast_preset_choices() -> list[str]:
|
||||
return list(CAST_PRESETS) + ["custom_counts"]
|
||||
|
||||
|
||||
def build_category_config_json(preset: str = "auto_weighted", subcategory: str = RANDOM_SUBCATEGORY) -> str:
|
||||
category, default_subcategory = CATEGORY_PRESETS.get(preset, CATEGORY_PRESETS["auto_weighted"])
|
||||
chosen_subcategory = subcategory if subcategory and subcategory != RANDOM_SUBCATEGORY else default_subcategory
|
||||
return json.dumps(
|
||||
{
|
||||
"preset": preset if preset in CATEGORY_PRESETS else "auto_weighted",
|
||||
"category": category,
|
||||
"subcategory": chosen_subcategory,
|
||||
},
|
||||
ensure_ascii=True,
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
|
||||
def parse_category_config(category_config: str | dict[str, Any] | None) -> tuple[str, str]:
|
||||
if not category_config:
|
||||
return CATEGORY_PRESETS["auto_weighted"]
|
||||
if isinstance(category_config, dict):
|
||||
raw = category_config
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(category_config))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid category_config JSON: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("category_config must be a JSON object")
|
||||
preset = str(raw.get("preset") or "auto_weighted")
|
||||
category, subcategory = CATEGORY_PRESETS.get(preset, CATEGORY_PRESETS["auto_weighted"])
|
||||
category = str(raw.get("category") or category)
|
||||
subcategory = str(raw.get("subcategory") or subcategory or RANDOM_SUBCATEGORY)
|
||||
return category, subcategory
|
||||
|
||||
|
||||
def build_cast_config_json(cast_mode: str = "mixed_couple", women_count: int = 1, men_count: int = 1) -> str:
|
||||
if cast_mode in CAST_PRESETS:
|
||||
women_count, men_count = CAST_PRESETS[cast_mode]
|
||||
else:
|
||||
women_count = max(0, min(12, int(women_count)))
|
||||
men_count = max(0, min(12, int(men_count)))
|
||||
if women_count + men_count == 0:
|
||||
women_count = 1
|
||||
cast_mode = "custom_counts"
|
||||
return json.dumps(
|
||||
{
|
||||
"cast_mode": cast_mode,
|
||||
"women_count": int(women_count),
|
||||
"men_count": int(men_count),
|
||||
},
|
||||
ensure_ascii=True,
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
|
||||
def parse_cast_config(cast_config: str | dict[str, Any] | None) -> dict[str, int | str]:
|
||||
if not cast_config:
|
||||
return {"cast_mode": "mixed_couple", "women_count": 1, "men_count": 1}
|
||||
if isinstance(cast_config, dict):
|
||||
raw = cast_config
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(cast_config))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid cast_config JSON: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("cast_config must be a JSON object")
|
||||
return json.loads(
|
||||
build_cast_config_json(
|
||||
str(raw.get("cast_mode") or "custom_counts"),
|
||||
raw.get("women_count", 1),
|
||||
raw.get("men_count", 1),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
_parse_category_config = parse_category_config
|
||||
_parse_cast_config = parse_cast_config
|
||||
@@ -0,0 +1,117 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from . import category_library as category_policy
|
||||
from . import generate_prompt_batches as g
|
||||
from . import row_item as row_item_policy
|
||||
except ImportError: # Allows local smoke tests with top-level imports.
|
||||
import category_library as category_policy
|
||||
import generate_prompt_batches as g
|
||||
import row_item as row_item_policy
|
||||
|
||||
|
||||
BUILTIN_CATEGORIES = [
|
||||
"auto_weighted",
|
||||
"auto_full",
|
||||
"woman",
|
||||
"man",
|
||||
"couple",
|
||||
"group_or_layout",
|
||||
"custom_random",
|
||||
]
|
||||
|
||||
_EXTENSIONS_APPLIED = False
|
||||
|
||||
|
||||
def list_from(value: Any) -> list[Any]:
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return [value]
|
||||
|
||||
|
||||
def unique_extend(target: list[Any], additions: list[Any]) -> None:
|
||||
seen = set()
|
||||
for item in target:
|
||||
try:
|
||||
seen.add(json.dumps(item, sort_keys=True))
|
||||
except TypeError:
|
||||
seen.add(repr(item))
|
||||
for item in additions:
|
||||
try:
|
||||
marker = json.dumps(item, sort_keys=True)
|
||||
except TypeError:
|
||||
marker = repr(item)
|
||||
if marker not in seen:
|
||||
target.append(item)
|
||||
seen.add(marker)
|
||||
|
||||
|
||||
def extension_targets() -> dict[str, tuple[list[Any], bool]]:
|
||||
return {
|
||||
"women_clothes": (g.WOMEN_CLOTHES, False),
|
||||
"women_clothes_minimal": (g.WOMEN_CLOTHES_MINIMAL, False),
|
||||
"men_clothes": (g.MEN_CLOTHES, False),
|
||||
"men_clothes_minimal": (g.MEN_CLOTHES_MINIMAL, False),
|
||||
"couple_outfits": (g.COUPLE_OUTFITS, False),
|
||||
"couple_outfits_minimal": (g.COUPLE_OUTFITS_MINIMAL, False),
|
||||
"poses": (g.POSES, False),
|
||||
"evocative_poses": (g.EVOCATIVE_POSES, False),
|
||||
"backside_poses": (g.BACKSIDE_POSES, False),
|
||||
"expressions": (g.EXPRESSIONS, False),
|
||||
"compositions": (g.COMPOSITIONS, False),
|
||||
"props": (g.PROPS, False),
|
||||
"figure_curvy": (g.FIGURE_CURVY, False),
|
||||
"figure_athletic": (g.FIGURE_ATHLETIC, False),
|
||||
"figure_bombshell": (g.FIGURE_BOMBSHELL, False),
|
||||
"scenes": (g.SCENES, True),
|
||||
"group_scenes": (g.GROUP_SCENES, True),
|
||||
"layouts_full": (g.LAYOUTS_FULL, True),
|
||||
"layouts_minimal": (g.LAYOUTS_MINIMAL, True),
|
||||
"group_compositions": (g.GROUP_COMPOSITIONS, False),
|
||||
"group_ages": (g.GROUP_AGES, False),
|
||||
}
|
||||
|
||||
|
||||
def apply_pool_extensions() -> None:
|
||||
global _EXTENSIONS_APPLIED
|
||||
if _EXTENSIONS_APPLIED:
|
||||
return
|
||||
targets = extension_targets()
|
||||
for path in category_policy.category_json_files():
|
||||
data = category_policy.read_category_json(path)
|
||||
extensions = data.get("pool_extensions", {})
|
||||
if not isinstance(extensions, dict):
|
||||
raise ValueError(f"pool_extensions in {path} must be an object")
|
||||
for target_name, additions in extensions.items():
|
||||
if target_name not in targets:
|
||||
known = ", ".join(sorted(targets))
|
||||
raise ValueError(f"Unknown pool extension '{target_name}' in {path}. Known: {known}")
|
||||
target, expects_pair = targets[target_name]
|
||||
normalized = (
|
||||
[row_item_policy.pair_from(item) for item in list_from(additions)]
|
||||
if expects_pair
|
||||
else [row_item_policy.item_text(item) for item in list_from(additions)]
|
||||
)
|
||||
unique_extend(target, normalized)
|
||||
g.EVOCATIVE_ALL = g.EVOCATIVE_POSES + g.BACKSIDE_POSES
|
||||
_EXTENSIONS_APPLIED = True
|
||||
|
||||
|
||||
def category_choices() -> list[str]:
|
||||
apply_pool_extensions()
|
||||
custom = [category["name"] for category in category_policy.load_category_library()]
|
||||
return BUILTIN_CATEGORIES + [name for name in custom if name not in BUILTIN_CATEGORIES]
|
||||
|
||||
|
||||
def subcategory_choices() -> list[str]:
|
||||
apply_pool_extensions()
|
||||
choices = [category_policy.RANDOM_SUBCATEGORY]
|
||||
for category in category_policy.load_category_library():
|
||||
for subcategory in category["subcategories"]:
|
||||
choices.append(category_policy.exact_subcategory_selector(category, subcategory))
|
||||
return choices
|
||||
@@ -0,0 +1,574 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent
|
||||
CATEGORY_DIR = ROOT_DIR / "categories"
|
||||
RANDOM_SUBCATEGORY = "random"
|
||||
|
||||
|
||||
def category_json_files() -> list[Path]:
|
||||
if not CATEGORY_DIR.exists():
|
||||
return []
|
||||
return sorted(path for path in CATEGORY_DIR.glob("*.json") if path.is_file())
|
||||
|
||||
|
||||
def read_category_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"{path} must contain a JSON object")
|
||||
return data
|
||||
|
||||
|
||||
def _slug(value: str) -> str:
|
||||
text = str(value or "").lower()
|
||||
text = re.sub(r"[^a-z0-9]+", "_", text)
|
||||
return text.strip("_")[:48] or "custom"
|
||||
|
||||
|
||||
def _list_from(value: Any) -> list[Any]:
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return [value]
|
||||
|
||||
|
||||
def _is_false(value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value is False
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ("false", "0", "no", "off")
|
||||
return False
|
||||
|
||||
|
||||
def _entry_text(item: Any) -> str:
|
||||
if isinstance(item, dict):
|
||||
return str(
|
||||
item.get("template")
|
||||
or item.get("prompt")
|
||||
or item.get("text")
|
||||
or item.get("description")
|
||||
or item.get("name")
|
||||
or ""
|
||||
).strip()
|
||||
return str(item).strip()
|
||||
|
||||
|
||||
def _unique_extend(target: list[Any], additions: list[Any]) -> None:
|
||||
seen = set()
|
||||
for item in target:
|
||||
try:
|
||||
seen.add(json.dumps(item, sort_keys=True))
|
||||
except TypeError:
|
||||
seen.add(repr(item))
|
||||
for item in additions:
|
||||
try:
|
||||
marker = json.dumps(item, sort_keys=True)
|
||||
except TypeError:
|
||||
marker = repr(item)
|
||||
if marker not in seen:
|
||||
target.append(item)
|
||||
seen.add(marker)
|
||||
|
||||
|
||||
def _weighted_choice(rng: random.Random, items: list[Any]) -> Any:
|
||||
if not items:
|
||||
raise ValueError("Cannot choose from an empty list")
|
||||
weights: list[float] = []
|
||||
for item in items:
|
||||
weight = item.get("weight", 1.0) if isinstance(item, dict) else 1.0
|
||||
try:
|
||||
weights.append(max(0.0, float(weight)))
|
||||
except (TypeError, ValueError):
|
||||
weights.append(1.0)
|
||||
total = sum(weights)
|
||||
if total <= 0:
|
||||
return items[rng.randrange(len(items))]
|
||||
pick = rng.random() * total
|
||||
running = 0.0
|
||||
for item, weight in zip(items, weights):
|
||||
running += weight
|
||||
if pick <= running:
|
||||
return item
|
||||
return items[-1]
|
||||
|
||||
|
||||
def template_list(category: dict[str, Any], subcategory: dict[str, Any], item: Any, key: str) -> list[Any]:
|
||||
if isinstance(item, dict) and key in item:
|
||||
return _list_from(item[key])
|
||||
if key in subcategory:
|
||||
return _list_from(subcategory[key])
|
||||
if key in category:
|
||||
return _list_from(category[key])
|
||||
return []
|
||||
|
||||
|
||||
def _constraint_int(entry: dict[str, Any], key: str) -> int | None:
|
||||
if key not in entry:
|
||||
return None
|
||||
try:
|
||||
return int(entry[key])
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _cast_requirement_matches(requirement: str, women_count: int, men_count: int) -> bool:
|
||||
total = women_count + men_count
|
||||
requirement = requirement.strip().lower()
|
||||
if requirement in ("", "any"):
|
||||
return True
|
||||
if requirement == "women_only":
|
||||
return women_count > 0 and men_count == 0
|
||||
if requirement == "men_only":
|
||||
return men_count > 0 and women_count == 0
|
||||
if requirement == "mixed":
|
||||
return women_count > 0 and men_count > 0
|
||||
if requirement == "has_women":
|
||||
return women_count > 0
|
||||
if requirement == "has_men":
|
||||
return men_count > 0
|
||||
if requirement == "solo":
|
||||
return total == 1
|
||||
if requirement == "couple":
|
||||
return total == 2
|
||||
if requirement == "threesome":
|
||||
return total == 3
|
||||
if requirement == "group":
|
||||
return total >= 4
|
||||
return True
|
||||
|
||||
|
||||
def _is_toy_assisted_double_couple_text(text: str) -> bool:
|
||||
text = text.lower()
|
||||
if "toy" not in text:
|
||||
return False
|
||||
return any(
|
||||
token in text
|
||||
for token in (
|
||||
"double penetration",
|
||||
"double-penetration",
|
||||
"vaginal and anal penetration",
|
||||
"second penetration point",
|
||||
"second point of contact",
|
||||
"second contact",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _heuristic_cast_compatible(text: str, women_count: int, men_count: int) -> bool:
|
||||
text = text.lower()
|
||||
if not text:
|
||||
return True
|
||||
total = women_count + men_count
|
||||
if total == 2 and women_count == 1 and men_count == 1:
|
||||
if "{double_act}" in text:
|
||||
return False
|
||||
if _is_toy_assisted_double_couple_text(text):
|
||||
return False
|
||||
if total == 1:
|
||||
solo_blocked_terms = (
|
||||
"partner",
|
||||
"partners",
|
||||
"two bodies",
|
||||
"three bodies",
|
||||
"bodies still pressed",
|
||||
"bodies pressed",
|
||||
"bodies tangled",
|
||||
"wet bodies",
|
||||
"chests heaving together",
|
||||
"straddling a partner",
|
||||
"shared climax",
|
||||
"between two",
|
||||
"from both sides",
|
||||
"front-and-back",
|
||||
"body contact",
|
||||
)
|
||||
if any(term in text for term in solo_blocked_terms):
|
||||
return False
|
||||
solo_toy_terms = ("toy", "dildo", "finger", "fingers", "self")
|
||||
if "penetration" in text and not any(term in text for term in solo_toy_terms):
|
||||
return False
|
||||
if total < 3 and "threesome" in text:
|
||||
return False
|
||||
if total != 3 and ("centered threesome" in text or "three-way" in text):
|
||||
return False
|
||||
if total < 3 and ("three bodies" in text or "center partner" in text or "center body" in text):
|
||||
return False
|
||||
if total < 4 and ("orgy" in text or "group sex" in text or "group-sex" in text or "group pile" in text):
|
||||
return False
|
||||
if total < 3 and (
|
||||
"double penetration" in text
|
||||
or "two partners penetrating" in text
|
||||
or "front-and-back penetration" in text
|
||||
or "one penis in pussy and one penis in ass" in text
|
||||
or "pussy and ass filled" in text
|
||||
or "vaginal and anal penetration at the same time" in text
|
||||
or "front-and-back double penetration" in text
|
||||
or "hardcore double penetration" in text
|
||||
or "kneeling double penetration" in text
|
||||
or "standing supported double penetration" in text
|
||||
or "deep double penetration" in text
|
||||
or "between two partners" in text
|
||||
or "from both sides" in text
|
||||
):
|
||||
toy_terms = ("strap-on", "strap on", "dildo", "toy", "finger")
|
||||
if not any(term in text for term in toy_terms):
|
||||
return False
|
||||
if men_count == 0:
|
||||
toy_terms = ("strap-on", "strap on", "dildo", "toy", "finger", "fingers")
|
||||
penetration_terms = (
|
||||
"vaginal penetration",
|
||||
"deep vaginal sex",
|
||||
"penetrative sex",
|
||||
"pussy penetration",
|
||||
"pussy stretched",
|
||||
"vaginal thrusting",
|
||||
"full-body penetrative",
|
||||
"close-contact vaginal",
|
||||
"penetration clearly visible",
|
||||
"explicit penetrative contact",
|
||||
)
|
||||
if any(term in text for term in penetration_terms) and not any(term in text for term in toy_terms):
|
||||
return False
|
||||
male_terms = (
|
||||
" penis",
|
||||
"penis ",
|
||||
"penises",
|
||||
"cum",
|
||||
"creampie",
|
||||
"facial",
|
||||
"blowjob",
|
||||
"fellatio",
|
||||
"deepthroat",
|
||||
"ejaculation",
|
||||
"semen",
|
||||
)
|
||||
if any(term in text for term in male_terms) and not any(term in text for term in toy_terms):
|
||||
return False
|
||||
elif men_count < 2 and "penises" in text:
|
||||
return False
|
||||
if women_count == 0:
|
||||
if "penetrative sex" in text and not any(term in text for term in ("anal", "ass", "male/male", "men")):
|
||||
return False
|
||||
female_terms = (
|
||||
"pussy",
|
||||
"vaginal",
|
||||
"vagina",
|
||||
"cunnilingus",
|
||||
"clit",
|
||||
"clitoris",
|
||||
"breasts",
|
||||
"breast ",
|
||||
"nipples",
|
||||
"nipple",
|
||||
"underboob",
|
||||
)
|
||||
if any(term in text for term in female_terms):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def compatible_entry(entry: Any, women_count: int, men_count: int) -> bool:
|
||||
if not isinstance(entry, dict):
|
||||
return _heuristic_cast_compatible(_entry_text(entry), women_count, men_count)
|
||||
total = women_count + men_count
|
||||
for key, value in (
|
||||
("min_women", women_count),
|
||||
("min_men", men_count),
|
||||
("min_people", total),
|
||||
):
|
||||
minimum = _constraint_int(entry, key)
|
||||
if minimum is not None and value < minimum:
|
||||
return False
|
||||
for key, value in (
|
||||
("max_women", women_count),
|
||||
("max_men", men_count),
|
||||
("max_people", total),
|
||||
):
|
||||
maximum = _constraint_int(entry, key)
|
||||
if maximum is not None and value > maximum:
|
||||
return False
|
||||
requirements = _list_from(entry.get("cast", [])) + _list_from(entry.get("requires", []))
|
||||
if requirements and not all(_cast_requirement_matches(str(req), women_count, men_count) for req in requirements):
|
||||
return False
|
||||
if any(key in entry for key in ("subcategories", "item_templates", "item_axes")):
|
||||
return True
|
||||
return _heuristic_cast_compatible(_entry_text(entry), women_count, men_count)
|
||||
|
||||
|
||||
def compatible_entries(entries: list[Any], women_count: int, men_count: int) -> list[Any]:
|
||||
filtered = [entry for entry in entries if compatible_entry(entry, women_count, men_count)]
|
||||
return filtered or entries
|
||||
|
||||
|
||||
def merged_axes(category: dict[str, Any], subcategory: dict[str, Any], item: Any) -> dict[str, list[Any]]:
|
||||
axes: dict[str, list[Any]] = {}
|
||||
for source in (category, subcategory, item if isinstance(item, dict) else None):
|
||||
if not isinstance(source, dict):
|
||||
continue
|
||||
raw_axes = source.get("item_axes", {})
|
||||
if raw_axes is None:
|
||||
continue
|
||||
if not isinstance(raw_axes, dict):
|
||||
raise ValueError("item_axes must be a JSON object")
|
||||
for key, values in raw_axes.items():
|
||||
axes[str(key)] = _list_from(values)
|
||||
return axes
|
||||
|
||||
|
||||
def _normalize_subcategories(category: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
raw = category.get("subcategories", [])
|
||||
if isinstance(raw, dict):
|
||||
raw = [
|
||||
{"name": name, **(value if isinstance(value, dict) else {"items": value})}
|
||||
for name, value in raw.items()
|
||||
]
|
||||
subcategories: list[dict[str, Any]] = []
|
||||
for entry in _list_from(raw):
|
||||
if isinstance(entry, str):
|
||||
sub = {"name": entry, "items": [entry]}
|
||||
elif isinstance(entry, dict):
|
||||
sub = dict(entry)
|
||||
else:
|
||||
raise ValueError(f"Subcategory must be an object or string: {entry!r}")
|
||||
name = str(sub.get("name") or sub.get("slug") or "General").strip()
|
||||
sub["name"] = name
|
||||
sub["slug"] = str(sub.get("slug") or _slug(name))
|
||||
if "items" not in sub and "prompts" in sub:
|
||||
sub["items"] = sub["prompts"]
|
||||
if "items" not in sub:
|
||||
sub["items"] = [name]
|
||||
subcategories.append(sub)
|
||||
if not subcategories:
|
||||
name = str(category.get("name") or "General")
|
||||
subcategories.append({"name": "General", "slug": "general", "items": [name]})
|
||||
return subcategories
|
||||
|
||||
|
||||
def _normalize_categories(raw_categories: Any) -> list[dict[str, Any]]:
|
||||
if isinstance(raw_categories, dict):
|
||||
iterable = [
|
||||
{"name": name, **(value if isinstance(value, dict) else {"subcategories": value})}
|
||||
for name, value in raw_categories.items()
|
||||
]
|
||||
else:
|
||||
iterable = _list_from(raw_categories)
|
||||
|
||||
categories: list[dict[str, Any]] = []
|
||||
for entry in iterable:
|
||||
if not isinstance(entry, dict):
|
||||
raise ValueError(f"Category must be an object: {entry!r}")
|
||||
category = dict(entry)
|
||||
name = str(category.get("name") or category.get("slug") or "Custom").strip()
|
||||
category["name"] = name
|
||||
category["slug"] = str(category.get("slug") or _slug(name))
|
||||
category["subcategories"] = _normalize_subcategories(category)
|
||||
categories.append(category)
|
||||
return categories
|
||||
|
||||
|
||||
def load_category_library() -> list[dict[str, Any]]:
|
||||
categories: list[dict[str, Any]] = []
|
||||
for path in category_json_files():
|
||||
data = read_category_json(path)
|
||||
categories.extend(_normalize_categories(data.get("categories", [])))
|
||||
return categories
|
||||
|
||||
|
||||
def load_named_pool_library(key: str) -> dict[str, list[Any]]:
|
||||
pools: dict[str, list[Any]] = {}
|
||||
for path in category_json_files():
|
||||
data = read_category_json(path)
|
||||
raw_pools = data.get(key, {})
|
||||
if not raw_pools:
|
||||
continue
|
||||
if not isinstance(raw_pools, dict):
|
||||
raise ValueError(f"{key} in {path} must be an object")
|
||||
for name, entries in raw_pools.items():
|
||||
pool_name = str(name).strip()
|
||||
if not pool_name:
|
||||
continue
|
||||
pools.setdefault(pool_name, [])
|
||||
_unique_extend(pools[pool_name], _list_from(entries))
|
||||
return pools
|
||||
|
||||
|
||||
def load_scene_pool_library() -> dict[str, list[Any]]:
|
||||
return load_named_pool_library("scene_pools")
|
||||
|
||||
|
||||
def load_expression_pool_library() -> dict[str, list[Any]]:
|
||||
return load_named_pool_library("expression_pools")
|
||||
|
||||
|
||||
def load_composition_pool_library() -> dict[str, list[Any]]:
|
||||
return load_named_pool_library("composition_pools")
|
||||
|
||||
|
||||
def find_category(categories: list[dict[str, Any]], name_or_slug: str) -> dict[str, Any] | None:
|
||||
wanted = name_or_slug.strip().lower()
|
||||
for category in categories:
|
||||
if category["name"].lower() == wanted or category["slug"].lower() == wanted:
|
||||
return category
|
||||
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]:
|
||||
women_count = max(0, int(women_count))
|
||||
men_count = max(0, int(men_count))
|
||||
if women_count + men_count == 0:
|
||||
women_count = 1
|
||||
return women_count, men_count
|
||||
|
||||
|
||||
def _counts_for_exact_subcategory(
|
||||
subcategory: dict[str, Any],
|
||||
women_count: int,
|
||||
men_count: int,
|
||||
) -> tuple[int, int]:
|
||||
women_count, men_count = _base_cast_counts(women_count, men_count)
|
||||
|
||||
min_women = _constraint_int(subcategory, "min_women")
|
||||
if min_women is not None and women_count < min_women:
|
||||
women_count = min_women
|
||||
min_men = _constraint_int(subcategory, "min_men")
|
||||
if min_men is not None and men_count < min_men:
|
||||
men_count = min_men
|
||||
|
||||
min_people = _constraint_int(subcategory, "min_people")
|
||||
if min_people is not None:
|
||||
missing = min_people - (women_count + men_count)
|
||||
if missing > 0:
|
||||
if women_count > 0 or men_count == 0:
|
||||
women_count += missing
|
||||
else:
|
||||
men_count += missing
|
||||
return women_count, men_count
|
||||
|
||||
|
||||
def find_subcategory(
|
||||
categories: list[dict[str, Any]],
|
||||
category_choice: str,
|
||||
subcategory_choice: str,
|
||||
category_rng: random.Random,
|
||||
subcategory_rng: random.Random,
|
||||
women_count: int = 1,
|
||||
men_count: int = 1,
|
||||
random_subcategory: str = RANDOM_SUBCATEGORY,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], int, int]:
|
||||
women_count, men_count = _base_cast_counts(women_count, men_count)
|
||||
if subcategory_choice and subcategory_choice != random_subcategory and " / " in subcategory_choice:
|
||||
exact_choice = split_exact_subcategory_choice(categories, subcategory_choice)
|
||||
if not exact_choice:
|
||||
category_name = str(subcategory_choice).split(" / ", 1)[0]
|
||||
raise ValueError(f"Unknown category in subcategory picker: {category_name}")
|
||||
category, subcategory_name = exact_choice
|
||||
wanted = subcategory_name.strip().lower()
|
||||
for subcategory in category["subcategories"]:
|
||||
if subcategory["name"].lower() == wanted or subcategory["slug"].lower() == wanted:
|
||||
adjusted_women_count, adjusted_men_count = _counts_for_exact_subcategory(
|
||||
subcategory,
|
||||
women_count,
|
||||
men_count,
|
||||
)
|
||||
if not compatible_entry(subcategory, adjusted_women_count, adjusted_men_count):
|
||||
raise ValueError(
|
||||
f"Subcategory '{subcategory['name']}' is not compatible with "
|
||||
f"women_count={women_count}, men_count={men_count}"
|
||||
)
|
||||
return category, subcategory, adjusted_women_count, adjusted_men_count
|
||||
raise ValueError(f"Unknown subcategory '{subcategory_name}' for category '{category['name']}'")
|
||||
|
||||
if category_choice == "custom_random":
|
||||
if not categories:
|
||||
raise ValueError("No custom categories found in categories/*.json")
|
||||
category = _weighted_choice(category_rng, categories)
|
||||
else:
|
||||
category = find_category(categories, category_choice)
|
||||
if not category:
|
||||
raise ValueError(f"Unknown custom category: {category_choice}")
|
||||
subcategories = compatible_entries(category["subcategories"], women_count, men_count)
|
||||
subcategory = _weighted_choice(subcategory_rng, subcategories)
|
||||
return category, subcategory, women_count, men_count
|
||||
|
||||
|
||||
def merged_field(category: dict[str, Any], subcategory: dict[str, Any], item: Any, key: str, default: Any = None) -> Any:
|
||||
if isinstance(item, dict) and key in item:
|
||||
return item[key]
|
||||
if key in subcategory:
|
||||
return subcategory[key]
|
||||
if key in category:
|
||||
return category[key]
|
||||
return default
|
||||
|
||||
|
||||
def _sources_with_inheritance(
|
||||
category: dict[str, Any],
|
||||
subcategory: dict[str, Any],
|
||||
item: Any,
|
||||
inherit_key: str,
|
||||
) -> tuple[Any, ...]:
|
||||
item_source = item if isinstance(item, dict) else None
|
||||
if item_source is not None and _is_false(item_source.get(inherit_key)):
|
||||
return (item_source,)
|
||||
if _is_false(subcategory.get(inherit_key)):
|
||||
return (subcategory, item_source)
|
||||
return (category, subcategory, item_source)
|
||||
|
||||
|
||||
def configured_pool(
|
||||
category: dict[str, Any],
|
||||
subcategory: dict[str, Any],
|
||||
item: Any,
|
||||
direct_key: str,
|
||||
pool_key: str,
|
||||
pool_library: dict[str, list[Any]],
|
||||
inherit_key: str,
|
||||
) -> list[Any]:
|
||||
entries: list[Any] = []
|
||||
singular_pool_key = pool_key[:-1] if pool_key.endswith("s") else pool_key
|
||||
for source in _sources_with_inheritance(category, subcategory, item, inherit_key):
|
||||
if not isinstance(source, dict):
|
||||
continue
|
||||
if direct_key in source:
|
||||
_unique_extend(entries, _list_from(source[direct_key]))
|
||||
refs = _list_from(source.get(singular_pool_key)) + _list_from(source.get(pool_key))
|
||||
for ref in refs:
|
||||
ref_name = str(ref).strip()
|
||||
if ref_name not in pool_library:
|
||||
raise ValueError(f"Unknown {singular_pool_key} '{ref_name}'")
|
||||
_unique_extend(entries, pool_library[ref_name])
|
||||
return entries
|
||||
@@ -0,0 +1,226 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from .hardcore_action_metadata import normalize_hardcore_action_family
|
||||
from .hardcore_position_config import normalize_hardcore_position_family, normalize_hardcore_position_values
|
||||
except ImportError: # Allows local smoke tests from the repository root.
|
||||
from hardcore_action_metadata import normalize_hardcore_action_family
|
||||
from hardcore_position_config import normalize_hardcore_position_family, normalize_hardcore_position_values
|
||||
|
||||
|
||||
TEMPLATE_METADATA_KEYS = (
|
||||
"action_family",
|
||||
"action_type",
|
||||
"family",
|
||||
"position_family",
|
||||
"position_key",
|
||||
"position_keys",
|
||||
"formatter_hint",
|
||||
)
|
||||
FORMATTER_HINT_ROUTES = ("all", "krea", "sdxl", "caption")
|
||||
FORMATTER_HINT_ROUTE_ALIASES = {
|
||||
"krea2": "krea",
|
||||
"naturalizer": "caption",
|
||||
"training_caption": "caption",
|
||||
}
|
||||
|
||||
|
||||
def template_metadata(item: Any) -> dict[str, Any]:
|
||||
if not isinstance(item, dict):
|
||||
return {}
|
||||
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:
|
||||
return normalize_hardcore_position_family(
|
||||
metadata.get("position_family") or metadata.get("family"),
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
def template_position_keys(metadata: dict[str, Any]) -> list[str]:
|
||||
keys: list[Any] = []
|
||||
if metadata.get("position_keys") is not None:
|
||||
raw_keys = metadata.get("position_keys")
|
||||
keys.extend(raw_keys if isinstance(raw_keys, list) else [raw_keys])
|
||||
if metadata.get("position_key") is not None:
|
||||
keys.append(metadata.get("position_key"))
|
||||
return normalize_hardcore_position_values(keys)
|
||||
|
||||
|
||||
def template_action_family(metadata: dict[str, Any]) -> str:
|
||||
return normalize_hardcore_action_family(metadata.get("action_family") or metadata.get("action_type"), "")
|
||||
|
||||
|
||||
def _list_from(value: Any) -> list[Any]:
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return [value]
|
||||
|
||||
|
||||
def _clean_hint(value: Any) -> str:
|
||||
return str(value or "").strip()
|
||||
|
||||
|
||||
def normalize_formatter_route(value: Any) -> str:
|
||||
route = re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().lower()).strip("_")
|
||||
route = FORMATTER_HINT_ROUTE_ALIASES.get(route, route)
|
||||
return route if route in FORMATTER_HINT_ROUTES else ""
|
||||
|
||||
|
||||
def formatter_hints(metadata: dict[str, Any]) -> dict[str, list[str]]:
|
||||
raw = metadata.get("formatter_hint")
|
||||
if raw is None:
|
||||
return {}
|
||||
normalized: dict[str, list[str]] = {}
|
||||
|
||||
def add(route: str, values: Any) -> None:
|
||||
route = normalize_formatter_route(route)
|
||||
if not route:
|
||||
return
|
||||
for value in _list_from(values):
|
||||
hint = _clean_hint(value)
|
||||
if hint and hint not in normalized.setdefault(route, []):
|
||||
normalized[route].append(hint)
|
||||
|
||||
if isinstance(raw, dict):
|
||||
for route, values in raw.items():
|
||||
add(str(route), values)
|
||||
else:
|
||||
add("all", raw)
|
||||
return {route: hints for route, hints in normalized.items() if hints}
|
||||
|
||||
|
||||
def formatter_hints_for_route(row_or_hints: Any, route: str) -> list[str]:
|
||||
route = normalize_formatter_route(route)
|
||||
if not route or not isinstance(row_or_hints, dict):
|
||||
return []
|
||||
|
||||
if isinstance(row_or_hints.get("formatter_hints"), dict):
|
||||
raw_hints = row_or_hints.get("formatter_hints") or {}
|
||||
elif "formatter_hint" in row_or_hints:
|
||||
raw_hints = formatter_hints(row_or_hints)
|
||||
elif row_or_hints and all(normalize_formatter_route(raw_route) for raw_route in row_or_hints):
|
||||
raw_hints = row_or_hints
|
||||
else:
|
||||
return []
|
||||
|
||||
normalized: dict[str, list[str]] = {}
|
||||
if isinstance(raw_hints, dict):
|
||||
for raw_route, values in raw_hints.items():
|
||||
normalized_route = normalize_formatter_route(raw_route)
|
||||
if not normalized_route:
|
||||
continue
|
||||
for value in _list_from(values):
|
||||
hint = _clean_hint(value)
|
||||
if hint and hint not in normalized.setdefault(normalized_route, []):
|
||||
normalized[normalized_route].append(hint)
|
||||
|
||||
hints: list[str] = []
|
||||
for raw_route in ("all", route):
|
||||
for hint in normalized.get(raw_route, []):
|
||||
if hint not in hints:
|
||||
hints.append(hint)
|
||||
return hints
|
||||
|
||||
|
||||
def merge_position_keys(primary: list[str], fallback: list[str]) -> list[str]:
|
||||
merged: list[str] = []
|
||||
for key in [*primary, *fallback]:
|
||||
if key and key not in merged:
|
||||
merged.append(key)
|
||||
return merged
|
||||
|
||||
|
||||
def _position_key_slug(value: Any) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().lower()).strip("_")
|
||||
|
||||
|
||||
def template_metadata_errors(metadata: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
raw_action_family = metadata.get("action_family") or metadata.get("action_type")
|
||||
if raw_action_family and not template_action_family(metadata):
|
||||
errors.append(f"unknown action_family/action_type: {raw_action_family}")
|
||||
raw_position_family = metadata.get("position_family") or metadata.get("family")
|
||||
if raw_position_family and not template_position_family(metadata):
|
||||
errors.append(f"unknown position_family/family: {raw_position_family}")
|
||||
raw_position_keys = []
|
||||
if metadata.get("position_keys") is not None:
|
||||
values = metadata.get("position_keys")
|
||||
raw_position_keys.extend(values if isinstance(values, list) else [values])
|
||||
if metadata.get("position_key") is not None:
|
||||
raw_position_keys.append(metadata.get("position_key"))
|
||||
normalized_keys = template_position_keys(metadata)
|
||||
invalid_keys = [
|
||||
str(value)
|
||||
for value in raw_position_keys
|
||||
if str(value or "").strip()
|
||||
and str(value or "").strip() != "any"
|
||||
and _position_key_slug(value) not in normalized_keys
|
||||
]
|
||||
if invalid_keys:
|
||||
errors.append("unknown position key(s): " + ", ".join(invalid_keys))
|
||||
raw_hint = metadata.get("formatter_hint")
|
||||
if raw_hint is not None:
|
||||
if isinstance(raw_hint, dict):
|
||||
for route, values in raw_hint.items():
|
||||
if not normalize_formatter_route(route):
|
||||
errors.append(f"unknown formatter_hint route: {route}")
|
||||
invalid_values = [
|
||||
repr(value)
|
||||
for value in _list_from(values)
|
||||
if not isinstance(value, str) or not value.strip()
|
||||
]
|
||||
if invalid_values:
|
||||
errors.append(f"invalid formatter_hint value(s) for {route}: " + ", ".join(invalid_values))
|
||||
else:
|
||||
invalid_values = [
|
||||
repr(value)
|
||||
for value in _list_from(raw_hint)
|
||||
if not isinstance(value, str) or not value.strip()
|
||||
]
|
||||
if invalid_values:
|
||||
errors.append("invalid formatter_hint value(s): " + ", ".join(invalid_values))
|
||||
return errors
|
||||
@@ -0,0 +1,268 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from . import character_config as character_policy
|
||||
from . import character_profile as character_profile_policy
|
||||
from . import character_slot as character_slot_policy
|
||||
from . import generate_prompt_batches as g
|
||||
from . import seed_config as seed_policy
|
||||
except ImportError: # Allows local smoke tests with top-level imports.
|
||||
import character_config as character_policy
|
||||
import character_profile as character_profile_policy
|
||||
import character_slot as character_slot_policy
|
||||
import generate_prompt_batches as g
|
||||
import seed_config as seed_policy
|
||||
|
||||
|
||||
def _choose(rng: random.Random, items: list[Any]) -> Any:
|
||||
return items[rng.randrange(len(items))]
|
||||
|
||||
|
||||
def slot_softcore_outfit(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
|
||||
if not slot:
|
||||
return ""
|
||||
outfit = character_policy.slot_value(slot.get("softcore_outfit"))
|
||||
if outfit:
|
||||
return outfit
|
||||
if rng is None:
|
||||
return ""
|
||||
return character_policy.characteristic_choice(
|
||||
character_policy.parse_characteristics_config(slot.get("characteristics")),
|
||||
"softcore_outfits",
|
||||
rng,
|
||||
)
|
||||
|
||||
|
||||
def slot_hardcore_clothing(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
|
||||
if not slot:
|
||||
return ""
|
||||
clothing = character_policy.slot_value(slot.get("hardcore_clothing"))
|
||||
if clothing:
|
||||
return clothing
|
||||
if rng is None:
|
||||
return ""
|
||||
return character_policy.characteristic_choice(
|
||||
character_policy.parse_characteristics_config(slot.get("characteristics")),
|
||||
"hardcore_clothing",
|
||||
rng,
|
||||
)
|
||||
|
||||
|
||||
def hair_descriptor_from_slot(base_hair: Any, slot: dict[str, Any], rng: random.Random) -> str:
|
||||
hair_config = character_policy.parse_hair_config(slot.get("hair_config"))
|
||||
color_choice = character_policy.normalize_hair_choice(slot.get("hair_color"), character_policy.CHARACTER_HAIR_COLOR_CHOICES)
|
||||
length_choice = character_policy.normalize_hair_choice(slot.get("hair_length"), character_policy.CHARACTER_HAIR_LENGTH_CHOICES)
|
||||
style_choice = character_policy.normalize_hair_choice(slot.get("hair_style"), character_policy.CHARACTER_HAIR_STYLE_CHOICES)
|
||||
color_options = hair_config.get("colors") or []
|
||||
length_options = hair_config.get("lengths") or []
|
||||
style_options = hair_config.get("styles") or []
|
||||
if (
|
||||
color_choice == "random"
|
||||
and length_choice == "random"
|
||||
and style_choice == "random"
|
||||
and not color_options
|
||||
and not length_options
|
||||
and not style_options
|
||||
):
|
||||
return ""
|
||||
if color_choice != "random":
|
||||
color_key = color_choice
|
||||
elif color_options:
|
||||
color_key = _choose(rng, color_options)
|
||||
else:
|
||||
color_key = character_policy.infer_hair_color_key(base_hair)
|
||||
|
||||
if length_choice != "random":
|
||||
length_key = length_choice
|
||||
elif length_options:
|
||||
length_key = _choose(rng, length_options)
|
||||
else:
|
||||
length_key = character_policy.infer_hair_length_key(base_hair)
|
||||
|
||||
if style_choice != "random":
|
||||
style_key = style_choice
|
||||
elif style_options:
|
||||
style_key = _choose(rng, style_options)
|
||||
else:
|
||||
style_key = character_policy.infer_hair_style_key(base_hair)
|
||||
if color_key == "random":
|
||||
color_key = character_policy.choose_hair_key(rng, character_policy.CHARACTER_HAIR_COLOR_CHOICES)
|
||||
if length_key == "random":
|
||||
length_key = character_policy.choose_hair_key(rng, character_policy.CHARACTER_HAIR_LENGTH_CHOICES)
|
||||
if style_key == "random":
|
||||
style_key = character_policy.choose_hair_key(rng, character_policy.CHARACTER_HAIR_STYLE_CHOICES)
|
||||
if length_key == "updo" and style_key not in ("ponytail", "braid", "braids", "bun", "messy_bun", "locs", "twists"):
|
||||
style_key = _choose(rng, ["ponytail", "braid", "bun", "messy_bun"])
|
||||
return character_policy.hair_phrase_from_parts(color_key, length_key, style_key)
|
||||
|
||||
|
||||
def appearance_for_subject(
|
||||
rng: random.Random,
|
||||
subject_type: str,
|
||||
ethnicity: str,
|
||||
figure: str,
|
||||
no_plus_women: bool,
|
||||
no_black: bool,
|
||||
) -> dict[str, str]:
|
||||
if subject_type == "single_any":
|
||||
subject_type = "woman" if rng.random() < 0.82 else "man"
|
||||
|
||||
if subject_type == "man":
|
||||
men_ethnicity = ethnicity if ethnicity else "any"
|
||||
subject, age, body, skin, hair, eyes = g.choose(rng, g.by_ethnicity(g.MEN, men_ethnicity))
|
||||
return {
|
||||
"subject_type": "man",
|
||||
"subject": subject,
|
||||
"subject_phrase": subject,
|
||||
"age": age,
|
||||
"body": body,
|
||||
"skin": skin,
|
||||
"hair": hair,
|
||||
"eyes": eyes,
|
||||
"body_phrase": f"{body} figure",
|
||||
}
|
||||
|
||||
subject, age, body, skin, hair, eyes = g.choose_woman(rng, ethnicity, no_plus_women, no_black)
|
||||
figure_note = g.choose(rng, g.figure_pool(figure))
|
||||
return {
|
||||
"subject_type": "woman",
|
||||
"subject": subject,
|
||||
"subject_phrase": subject,
|
||||
"age": age,
|
||||
"body": body,
|
||||
"skin": skin,
|
||||
"hair": hair,
|
||||
"eyes": eyes,
|
||||
"body_phrase": character_profile_policy.body_phrase(body, figure_note),
|
||||
"figure": figure_note,
|
||||
}
|
||||
|
||||
|
||||
def context_from_character_slot(
|
||||
rng: random.Random,
|
||||
slot: dict[str, Any],
|
||||
subject_type: str,
|
||||
ethnicity: str,
|
||||
figure: str,
|
||||
no_plus_women: bool,
|
||||
no_black: bool,
|
||||
) -> dict[str, Any]:
|
||||
slot_ethnicity = character_policy.slot_value(slot.get("ethnicity"))
|
||||
slot_body = character_policy.slot_value(slot.get("body"))
|
||||
effective_ethnicity = slot_ethnicity or ethnicity
|
||||
effective_figure = character_slot_policy.slot_effective_figure(slot, subject_type, figure)
|
||||
effective_no_plus = bool(no_plus_women) and not slot_body
|
||||
effective_no_black = bool(no_black) and not slot_ethnicity
|
||||
appearance_rng = character_slot_policy.slot_context_rng(slot, rng)
|
||||
context = appearance_for_subject(
|
||||
appearance_rng,
|
||||
subject_type,
|
||||
effective_ethnicity,
|
||||
effective_figure,
|
||||
effective_no_plus,
|
||||
effective_no_black,
|
||||
)
|
||||
|
||||
characteristics = character_policy.parse_characteristics_config(slot.get("characteristics"))
|
||||
age = character_policy.slot_value(slot.get("age")) or character_policy.characteristic_choice(characteristics, "ages", appearance_rng)
|
||||
body_phrase = character_policy.slot_value(slot.get("body_phrase"))
|
||||
if not slot_body:
|
||||
slot_body = character_policy.characteristic_choice(characteristics, "bodies", appearance_rng)
|
||||
if age:
|
||||
context["age"] = age
|
||||
if slot_body:
|
||||
context["body"] = slot_body
|
||||
if subject_type == "woman":
|
||||
context["body_phrase"] = character_profile_policy.body_phrase(slot_body, context.get("figure", ""))
|
||||
else:
|
||||
context["body_phrase"] = f"{slot_body} figure"
|
||||
if body_phrase:
|
||||
context["body_phrase"] = body_phrase
|
||||
skin_value = character_policy.slot_value(slot.get("skin"))
|
||||
if skin_value:
|
||||
context["skin"] = skin_value
|
||||
eyes_value = character_policy.slot_value(slot.get("eyes"))
|
||||
if not eyes_value:
|
||||
eyes_value = character_policy.eye_phrase_from_key(character_policy.characteristic_choice(characteristics, "eyes", appearance_rng))
|
||||
if eyes_value:
|
||||
context["eyes"] = eyes_value
|
||||
hair_value = character_policy.slot_value(slot.get("hair"))
|
||||
if hair_value:
|
||||
context["hair"] = hair_value
|
||||
else:
|
||||
hair_descriptor = hair_descriptor_from_slot(context.get("hair"), slot, appearance_rng)
|
||||
if hair_descriptor:
|
||||
context["hair"] = hair_descriptor
|
||||
context["descriptor_detail"] = character_policy.normalize_descriptor_detail(slot.get("descriptor_detail"))
|
||||
context["presence_mode"] = character_policy.normalize_presence_mode(slot.get("presence_mode"), subject_type)
|
||||
context["expression_enabled"] = character_slot_policy.slot_expression_enabled(slot)
|
||||
expression_intensity = character_slot_policy.slot_expression_intensity(slot)
|
||||
if expression_intensity is not None:
|
||||
context["expression_intensity"] = expression_intensity
|
||||
context["subject_type"] = subject_type
|
||||
context["subject"] = subject_type
|
||||
context["subject_phrase"] = subject_type
|
||||
return context
|
||||
|
||||
|
||||
def character_context_for_label(
|
||||
label: str,
|
||||
label_map: dict[str, dict[str, Any]],
|
||||
rng: random.Random,
|
||||
ethnicity: str,
|
||||
figure: str,
|
||||
no_plus_women: bool,
|
||||
no_black: bool,
|
||||
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
||||
subject_type = "man" if label.startswith("Man ") else "woman"
|
||||
slot = label_map.get(label)
|
||||
if slot:
|
||||
return context_from_character_slot(rng, slot, subject_type, ethnicity, figure, no_plus_women, no_black), slot
|
||||
return appearance_for_subject(rng, subject_type, ethnicity, figure, no_plus_women, no_black), None
|
||||
|
||||
|
||||
def apply_character_context_to_row(row: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
||||
for key in (
|
||||
"subject_type",
|
||||
"subject",
|
||||
"subject_phrase",
|
||||
"age",
|
||||
"body",
|
||||
"body_phrase",
|
||||
"skin",
|
||||
"hair",
|
||||
"eyes",
|
||||
"figure",
|
||||
"descriptor_detail",
|
||||
"presence_mode",
|
||||
"expression_enabled",
|
||||
"expression_intensity",
|
||||
):
|
||||
value = context.get(key)
|
||||
if value is not None and value != "":
|
||||
row[key] = value
|
||||
if context.get("age"):
|
||||
row["age_band"] = context["age"]
|
||||
return row
|
||||
|
||||
|
||||
def row_from_character_slot(character_slot: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
slots = character_slot_policy.parse_character_cast(character_slot)
|
||||
if not slots:
|
||||
return {}
|
||||
slot = slots[-1]
|
||||
if character_slot_policy.slot_seed(slot) >= 0:
|
||||
subject_type = str(slot.get("subject_type") or "woman")
|
||||
return context_from_character_slot(
|
||||
random.Random(seed_policy.row_seed(character_slot_policy.slot_seed(slot), 1, 719)),
|
||||
slot,
|
||||
subject_type,
|
||||
"any",
|
||||
"curvy",
|
||||
False,
|
||||
False,
|
||||
)
|
||||
return slot
|
||||
@@ -0,0 +1,688 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
CHARACTER_LABEL_CHOICES = [
|
||||
"auto_chain",
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"H",
|
||||
"I",
|
||||
"J",
|
||||
"K",
|
||||
"L",
|
||||
]
|
||||
CHARACTER_AGE_CHOICES = (
|
||||
["random", "manual"]
|
||||
+ [f"{age}-year-old adult" for age in range(21, 86)]
|
||||
+ [
|
||||
"late 20s adult",
|
||||
"early 30s adult",
|
||||
"mid 30s adult",
|
||||
"late 30s adult",
|
||||
"early 40s adult",
|
||||
"mid 40s adult",
|
||||
"late 40s adult",
|
||||
"early 50s adult",
|
||||
"mid 50s adult",
|
||||
"late 50s adult",
|
||||
"early 60s adult",
|
||||
"mid 60s adult",
|
||||
"late 60s adult",
|
||||
"early 70s adult",
|
||||
"mid 70s adult",
|
||||
"late 70s adult",
|
||||
"early 80s adult",
|
||||
]
|
||||
)
|
||||
CHARACTER_BODY_CHOICES = [
|
||||
"random",
|
||||
"manual",
|
||||
"slim",
|
||||
"petite adult",
|
||||
"toned",
|
||||
"athletic",
|
||||
"average",
|
||||
"curvy",
|
||||
"soft curvy",
|
||||
"curvy athletic",
|
||||
"hourglass",
|
||||
"slim busty",
|
||||
"busty",
|
||||
"busty curvy",
|
||||
"voluptuous",
|
||||
"plus-size",
|
||||
"heavyset",
|
||||
"fat",
|
||||
"stocky",
|
||||
"broad",
|
||||
"muscular",
|
||||
]
|
||||
CHARACTER_WOMAN_BODY_CHOICES = [
|
||||
"random",
|
||||
"manual",
|
||||
"slim",
|
||||
"petite adult",
|
||||
"toned",
|
||||
"athletic",
|
||||
"average",
|
||||
"curvy",
|
||||
"soft curvy",
|
||||
"curvy athletic",
|
||||
"hourglass",
|
||||
"slim busty",
|
||||
"busty",
|
||||
"busty curvy",
|
||||
"voluptuous",
|
||||
"plus-size",
|
||||
"heavyset",
|
||||
"fat",
|
||||
]
|
||||
CHARACTER_MAN_BODY_CHOICES = [
|
||||
"random",
|
||||
"manual",
|
||||
"slim",
|
||||
"lean",
|
||||
"lean athletic",
|
||||
"toned",
|
||||
"average",
|
||||
"athletic",
|
||||
"muscular",
|
||||
"broad",
|
||||
"broad-shouldered",
|
||||
"stocky",
|
||||
"heavyset",
|
||||
"fat",
|
||||
]
|
||||
CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "minimal"]
|
||||
CHARACTER_PRESENCE_CHOICES = ["visible", "pov"]
|
||||
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
|
||||
CHARACTER_SLOT_SEED_MAX = 0xFFFFFFFF
|
||||
CHARACTER_FIGURE_CHOICES = ["random", "curvy", "balanced", "bombshell"]
|
||||
CHARACTER_HAIR_COLOR_CHOICES = [
|
||||
"random",
|
||||
"black",
|
||||
"brown",
|
||||
"dark_brown",
|
||||
"chestnut",
|
||||
"auburn",
|
||||
"copper",
|
||||
"red",
|
||||
"blonde",
|
||||
"platinum_blonde",
|
||||
"ash_blonde",
|
||||
"honey_blonde",
|
||||
"strawberry_blonde",
|
||||
"dark_blonde",
|
||||
"silver_gray",
|
||||
"white",
|
||||
]
|
||||
CHARACTER_HAIR_LENGTH_CHOICES = [
|
||||
"random",
|
||||
"very_short",
|
||||
"short",
|
||||
"bob_lob",
|
||||
"shoulder_length",
|
||||
"medium",
|
||||
"long",
|
||||
"very_long",
|
||||
"updo",
|
||||
]
|
||||
CHARACTER_HAIR_STYLE_CHOICES = [
|
||||
"random",
|
||||
"straight",
|
||||
"waves",
|
||||
"loose_waves",
|
||||
"curls",
|
||||
"tight_curls",
|
||||
"pixie_cut",
|
||||
"bob",
|
||||
"lob",
|
||||
"shag",
|
||||
"ponytail",
|
||||
"braid",
|
||||
"braids",
|
||||
"bun",
|
||||
"messy_bun",
|
||||
"locs",
|
||||
"twists",
|
||||
"afro",
|
||||
"natural_curls",
|
||||
"wet_hair",
|
||||
"slicked_back",
|
||||
]
|
||||
CHARACTER_EYE_COLOR_CHOICES = [
|
||||
"random",
|
||||
"blue",
|
||||
"pale_blue",
|
||||
"ice_blue",
|
||||
"blue_gray",
|
||||
"green",
|
||||
"emerald_green",
|
||||
"hazel",
|
||||
"light_hazel",
|
||||
"green_hazel",
|
||||
"amber",
|
||||
"amber_brown",
|
||||
"honey_brown",
|
||||
"brown",
|
||||
"deep_brown",
|
||||
"dark_brown",
|
||||
"dark",
|
||||
"gray",
|
||||
"gray_brown",
|
||||
]
|
||||
CHARACTER_CHARACTERISTIC_AXES = {
|
||||
"ages": CHARACTER_AGE_CHOICES,
|
||||
"bodies": list(dict.fromkeys([*CHARACTER_BODY_CHOICES, *CHARACTER_WOMAN_BODY_CHOICES, *CHARACTER_MAN_BODY_CHOICES])),
|
||||
"eyes": CHARACTER_EYE_COLOR_CHOICES,
|
||||
}
|
||||
|
||||
|
||||
def character_label_choices() -> list[str]:
|
||||
return list(CHARACTER_LABEL_CHOICES)
|
||||
|
||||
|
||||
def character_age_choices() -> list[str]:
|
||||
return list(CHARACTER_AGE_CHOICES)
|
||||
|
||||
|
||||
def character_body_choices() -> list[str]:
|
||||
return list(CHARACTER_BODY_CHOICES)
|
||||
|
||||
|
||||
def character_woman_body_choices() -> list[str]:
|
||||
return list(CHARACTER_WOMAN_BODY_CHOICES)
|
||||
|
||||
|
||||
def character_man_body_choices() -> list[str]:
|
||||
return list(CHARACTER_MAN_BODY_CHOICES)
|
||||
|
||||
|
||||
def character_descriptor_detail_choices() -> list[str]:
|
||||
return list(CHARACTER_DESCRIPTOR_DETAIL_CHOICES)
|
||||
|
||||
|
||||
def character_presence_choices() -> list[str]:
|
||||
return list(CHARACTER_PRESENCE_CHOICES)
|
||||
|
||||
|
||||
def character_figure_choices() -> list[str]:
|
||||
return list(CHARACTER_FIGURE_CHOICES)
|
||||
|
||||
|
||||
def character_hair_color_choices() -> list[str]:
|
||||
return list(CHARACTER_HAIR_COLOR_CHOICES)
|
||||
|
||||
|
||||
def character_hair_length_choices() -> list[str]:
|
||||
return list(CHARACTER_HAIR_LENGTH_CHOICES)
|
||||
|
||||
|
||||
def character_hair_style_choices() -> list[str]:
|
||||
return list(CHARACTER_HAIR_STYLE_CHOICES)
|
||||
|
||||
|
||||
def character_eye_color_choices() -> list[str]:
|
||||
return list(CHARACTER_EYE_COLOR_CHOICES)
|
||||
|
||||
|
||||
def slot_value(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
if text.lower() in CHARACTER_RANDOM_TOKENS:
|
||||
return ""
|
||||
return text
|
||||
|
||||
|
||||
def normalize_descriptor_detail(value: Any) -> str:
|
||||
text = str(value or "auto").strip()
|
||||
return text if text in CHARACTER_DESCRIPTOR_DETAIL_CHOICES else "auto"
|
||||
|
||||
|
||||
def normalize_presence_mode(value: Any, subject_type: str) -> str:
|
||||
text = str(value or "visible").strip().lower()
|
||||
if text not in CHARACTER_PRESENCE_CHOICES:
|
||||
text = "visible"
|
||||
if subject_type != "man":
|
||||
return "visible"
|
||||
return text
|
||||
|
||||
|
||||
def normalize_slot_seed(value: Any) -> int:
|
||||
try:
|
||||
seed = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return -1
|
||||
if seed < 0:
|
||||
return -1
|
||||
return min(seed, CHARACTER_SLOT_SEED_MAX)
|
||||
|
||||
|
||||
def empty_characteristics_config() -> dict[str, Any]:
|
||||
return {
|
||||
"config_type": "characteristics",
|
||||
"ages": [],
|
||||
"bodies": [],
|
||||
"eyes": [],
|
||||
"softcore_outfits": [],
|
||||
"hardcore_clothing": [],
|
||||
}
|
||||
|
||||
|
||||
def normalize_characteristic_choice(value: Any, choices: list[str] | tuple[str, ...]) -> str:
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
normalized = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
|
||||
for choice in choices:
|
||||
if normalized == re.sub(r"[^a-z0-9]+", "_", str(choice).lower()).strip("_"):
|
||||
return str(choice)
|
||||
return ""
|
||||
|
||||
|
||||
def normalize_characteristic_values(
|
||||
values: Any,
|
||||
choices: list[str] | tuple[str, ...] | None = None,
|
||||
*,
|
||||
allow_free_text: bool = False,
|
||||
) -> list[str]:
|
||||
if isinstance(values, str):
|
||||
raw_values = [part.strip() for part in re.split(r"[\n;]+", values) if part.strip()]
|
||||
if len(raw_values) == 1 and "," in raw_values[0] and not allow_free_text:
|
||||
raw_values = [part.strip() for part in raw_values[0].split(",") if part.strip()]
|
||||
elif isinstance(values, (list, tuple, set)):
|
||||
raw_values = list(values)
|
||||
else:
|
||||
raw_values = []
|
||||
normalized: list[str] = []
|
||||
for raw_value in raw_values:
|
||||
value = str(raw_value or "").strip() if choices is None else normalize_characteristic_choice(raw_value, choices)
|
||||
if not value or value in ("random", "manual"):
|
||||
continue
|
||||
if value not in normalized:
|
||||
normalized.append(value)
|
||||
return normalized
|
||||
|
||||
|
||||
def parse_characteristics_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not value:
|
||||
return empty_characteristics_config()
|
||||
if isinstance(value, dict):
|
||||
raw = value
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(value))
|
||||
except json.JSONDecodeError:
|
||||
return empty_characteristics_config()
|
||||
if not isinstance(raw, dict):
|
||||
return empty_characteristics_config()
|
||||
return {
|
||||
"config_type": "characteristics",
|
||||
"ages": normalize_characteristic_values(raw.get("ages"), CHARACTER_AGE_CHOICES),
|
||||
"bodies": normalize_characteristic_values(raw.get("bodies"), CHARACTER_CHARACTERISTIC_AXES["bodies"]),
|
||||
"eyes": normalize_characteristic_values(raw.get("eyes"), CHARACTER_EYE_COLOR_CHOICES),
|
||||
"softcore_outfits": normalize_characteristic_values(raw.get("softcore_outfits"), None, allow_free_text=True),
|
||||
"hardcore_clothing": normalize_characteristic_values(raw.get("hardcore_clothing"), None, allow_free_text=True),
|
||||
}
|
||||
|
||||
|
||||
def characteristics_summary(config: dict[str, Any]) -> str:
|
||||
parts = []
|
||||
for key, label in (
|
||||
("ages", "ages"),
|
||||
("bodies", "bodies"),
|
||||
("eyes", "eyes"),
|
||||
("softcore_outfits", "soft_outfits"),
|
||||
("hardcore_clothing", "hard_clothing"),
|
||||
):
|
||||
values = config.get(key) or []
|
||||
if not values:
|
||||
continue
|
||||
if key in ("softcore_outfits", "hardcore_clothing"):
|
||||
parts.append(f"{label}={len(values)}")
|
||||
else:
|
||||
parts.append(f"{label}={','.join(values)}")
|
||||
return "; ".join(parts) if parts else "characteristics unrestricted"
|
||||
|
||||
|
||||
def build_characteristics_config_json(
|
||||
characteristics: str | dict[str, Any] | None = "",
|
||||
axis: str = "ages",
|
||||
selected_values: list[str] | tuple[str, ...] | str | None = None,
|
||||
combine_mode: str = "replace_axis",
|
||||
) -> str:
|
||||
config = parse_characteristics_config(characteristics)
|
||||
axis_key = str(axis or "").strip().lower()
|
||||
if axis_key not in config:
|
||||
config["summary"] = characteristics_summary(config)
|
||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||
choices = CHARACTER_CHARACTERISTIC_AXES.get(axis_key)
|
||||
values = normalize_characteristic_values(
|
||||
selected_values,
|
||||
choices,
|
||||
allow_free_text=choices is None,
|
||||
)
|
||||
if combine_mode == "add_to_axis":
|
||||
existing = list(config.get(axis_key) or [])
|
||||
for value in values:
|
||||
if value not in existing:
|
||||
existing.append(value)
|
||||
config[axis_key] = existing
|
||||
else:
|
||||
config[axis_key] = values
|
||||
config["summary"] = characteristics_summary(config)
|
||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def characteristic_choice(config: dict[str, Any], key: str, rng: random.Random) -> str:
|
||||
values = config.get(key) or []
|
||||
return values[rng.randrange(len(values))] if values else ""
|
||||
|
||||
|
||||
def eye_phrase_from_key(key: str) -> str:
|
||||
return {
|
||||
"blue": "blue eyes",
|
||||
"pale_blue": "pale blue eyes",
|
||||
"ice_blue": "ice blue eyes",
|
||||
"blue_gray": "blue-gray eyes",
|
||||
"green": "green eyes",
|
||||
"emerald_green": "emerald green eyes",
|
||||
"hazel": "hazel eyes",
|
||||
"light_hazel": "light hazel eyes",
|
||||
"green_hazel": "green-hazel eyes",
|
||||
"amber": "amber eyes",
|
||||
"amber_brown": "amber-brown eyes",
|
||||
"honey_brown": "honey-brown eyes",
|
||||
"brown": "brown eyes",
|
||||
"deep_brown": "deep brown eyes",
|
||||
"dark_brown": "dark brown eyes",
|
||||
"dark": "dark eyes",
|
||||
"gray": "gray eyes",
|
||||
"gray_brown": "gray-brown eyes",
|
||||
}.get(key, "")
|
||||
|
||||
|
||||
def normalize_hair_choice(value: Any, choices: list[str]) -> str:
|
||||
text = str(value or "random").strip().lower().replace("-", "_").replace(" ", "_")
|
||||
return text if text in choices else "random"
|
||||
|
||||
|
||||
def infer_hair_color_key(text: Any) -> str:
|
||||
value = str(text or "").lower()
|
||||
checks = (
|
||||
("platinum_blonde", ("platinum-blonde", "platinum blonde", "platinum")),
|
||||
("strawberry_blonde", ("strawberry-blonde", "strawberry blonde")),
|
||||
("honey_blonde", ("honey-blonde", "honey blonde")),
|
||||
("ash_blonde", ("ash-blonde", "ash blonde")),
|
||||
("dark_blonde", ("dark-blonde", "dark blonde")),
|
||||
(
|
||||
"blonde",
|
||||
(
|
||||
"light-blonde",
|
||||
"light blonde",
|
||||
"blonde",
|
||||
"flaxen",
|
||||
"wheat-blonde",
|
||||
"wheat blonde",
|
||||
"beige-blonde",
|
||||
"beige blonde",
|
||||
"sandy-blonde",
|
||||
"sandy blonde",
|
||||
),
|
||||
),
|
||||
("silver_gray", ("silver-gray", "silver grey", "silver", "gray", "grey")),
|
||||
("dark_brown", ("dark-brown", "dark brown", "espresso")),
|
||||
("chestnut", ("chestnut",)),
|
||||
("auburn", ("auburn",)),
|
||||
("copper", ("copper",)),
|
||||
("red", ("red hair", "redhead")),
|
||||
("black", ("black",)),
|
||||
("brown", ("brown", "brunette", "caramel")),
|
||||
("white", ("white",)),
|
||||
)
|
||||
for key, tokens in checks:
|
||||
if any(token in value for token in tokens):
|
||||
return key
|
||||
return "random"
|
||||
|
||||
|
||||
def infer_hair_length_key(text: Any) -> str:
|
||||
value = str(text or "").lower()
|
||||
if any(token in value for token in ("very long", "waist-length", "hip-length")):
|
||||
return "very_long"
|
||||
if "long" in value:
|
||||
return "long"
|
||||
if "shoulder-length" in value or "shoulder length" in value:
|
||||
return "shoulder_length"
|
||||
if "medium-length" in value or "medium length" in value:
|
||||
return "medium"
|
||||
if any(token in value for token in ("bob", "lob")):
|
||||
return "bob_lob"
|
||||
if any(token in value for token in ("pixie", "short", "cropped", "tapered")):
|
||||
return "short"
|
||||
if any(token in value for token in ("bun", "updo")):
|
||||
return "updo"
|
||||
return "random"
|
||||
|
||||
|
||||
def infer_hair_style_key(text: Any) -> str:
|
||||
value = str(text or "").lower()
|
||||
checks = (
|
||||
("pixie_cut", ("pixie",)),
|
||||
("messy_bun", ("messy bun",)),
|
||||
("bun", ("bun", "updo")),
|
||||
("ponytail", ("ponytail",)),
|
||||
("braids", ("braids", "box braids", "cornrow")),
|
||||
("braid", ("braid",)),
|
||||
("locs", ("locs", "dreadlocks")),
|
||||
("twists", ("twists",)),
|
||||
("afro", ("afro",)),
|
||||
("natural_curls", ("natural curls", "natural coils", "coils")),
|
||||
("tight_curls", ("tight curls", "tight coils")),
|
||||
("curls", ("curls", "curly")),
|
||||
("loose_waves", ("loose waves",)),
|
||||
("waves", ("waves", "wavy")),
|
||||
("lob", ("lob",)),
|
||||
("bob", ("bob",)),
|
||||
("shag", ("shag",)),
|
||||
("wet_hair", ("wet hair", "damp hair")),
|
||||
("slicked_back", ("slicked-back", "slicked back")),
|
||||
("straight", ("straight", "sleek")),
|
||||
)
|
||||
for key, tokens in checks:
|
||||
if any(token in value for token in tokens):
|
||||
return key
|
||||
return "random"
|
||||
|
||||
|
||||
def choose_hair_key(rng: random.Random, choices: list[str]) -> str:
|
||||
pool = [choice for choice in choices if choice != "random"]
|
||||
return pool[rng.randrange(len(pool))] if pool else "random"
|
||||
|
||||
|
||||
def normalize_hair_values(values: Any, choices: list[str]) -> list[str]:
|
||||
if isinstance(values, str):
|
||||
raw_values = [part.strip() for part in re.split(r"[,;\n]+", values) if part.strip()]
|
||||
elif isinstance(values, (list, tuple, set)):
|
||||
raw_values = list(values)
|
||||
else:
|
||||
raw_values = []
|
||||
normalized: list[str] = []
|
||||
for value in raw_values:
|
||||
key = normalize_hair_choice(value, choices)
|
||||
if key != "random" and key not in normalized:
|
||||
normalized.append(key)
|
||||
return normalized
|
||||
|
||||
|
||||
def empty_hair_config() -> dict[str, Any]:
|
||||
return {"config_type": "hair_characteristics", "colors": [], "lengths": [], "styles": []}
|
||||
|
||||
|
||||
def parse_hair_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not value:
|
||||
return empty_hair_config()
|
||||
if isinstance(value, dict):
|
||||
raw = value
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(value))
|
||||
except json.JSONDecodeError:
|
||||
return empty_hair_config()
|
||||
if not isinstance(raw, dict):
|
||||
return empty_hair_config()
|
||||
return {
|
||||
"config_type": "hair_characteristics",
|
||||
"colors": normalize_hair_values(raw.get("colors"), CHARACTER_HAIR_COLOR_CHOICES),
|
||||
"lengths": normalize_hair_values(raw.get("lengths"), CHARACTER_HAIR_LENGTH_CHOICES),
|
||||
"styles": normalize_hair_values(raw.get("styles"), CHARACTER_HAIR_STYLE_CHOICES),
|
||||
}
|
||||
|
||||
|
||||
def hair_config_summary(config: dict[str, Any]) -> str:
|
||||
parts = []
|
||||
for label, key in (("colors", "colors"), ("lengths", "lengths"), ("styles", "styles")):
|
||||
values = config.get(key) or []
|
||||
if values:
|
||||
parts.append(f"{label}={','.join(values)}")
|
||||
return "; ".join(parts) if parts else "hair unrestricted"
|
||||
|
||||
|
||||
def build_hair_config_json(
|
||||
hair_config: str | dict[str, Any] | None = "",
|
||||
axis: str = "color",
|
||||
selected_values: list[str] | tuple[str, ...] | str | None = None,
|
||||
combine_mode: str = "replace_axis",
|
||||
) -> str:
|
||||
config = parse_hair_config(hair_config)
|
||||
axis_key = {"color": "colors", "length": "lengths", "style": "styles"}.get(str(axis or "").strip().lower())
|
||||
choice_map = {
|
||||
"colors": CHARACTER_HAIR_COLOR_CHOICES,
|
||||
"lengths": CHARACTER_HAIR_LENGTH_CHOICES,
|
||||
"styles": CHARACTER_HAIR_STYLE_CHOICES,
|
||||
}
|
||||
if axis_key:
|
||||
values = normalize_hair_values(selected_values, choice_map[axis_key])
|
||||
if combine_mode == "add_to_axis":
|
||||
existing = list(config.get(axis_key) or [])
|
||||
for value in values:
|
||||
if value not in existing:
|
||||
existing.append(value)
|
||||
config[axis_key] = existing
|
||||
else:
|
||||
config[axis_key] = values
|
||||
config["summary"] = hair_config_summary(config)
|
||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def hair_color_text(key: str) -> str:
|
||||
return {
|
||||
"black": "black",
|
||||
"brown": "brown",
|
||||
"dark_brown": "dark-brown",
|
||||
"chestnut": "chestnut",
|
||||
"auburn": "auburn",
|
||||
"copper": "copper",
|
||||
"red": "red",
|
||||
"blonde": "blonde",
|
||||
"platinum_blonde": "platinum-blonde",
|
||||
"ash_blonde": "ash-blonde",
|
||||
"honey_blonde": "honey-blonde",
|
||||
"strawberry_blonde": "strawberry-blonde",
|
||||
"dark_blonde": "dark-blonde",
|
||||
"silver_gray": "silver-gray",
|
||||
"white": "white",
|
||||
}.get(key, "brown")
|
||||
|
||||
|
||||
def hair_length_text(key: str) -> str:
|
||||
return {
|
||||
"very_short": "very short",
|
||||
"short": "short",
|
||||
"bob_lob": "",
|
||||
"shoulder_length": "shoulder-length",
|
||||
"medium": "medium-length",
|
||||
"long": "long",
|
||||
"very_long": "very long",
|
||||
"updo": "",
|
||||
}.get(key, "")
|
||||
|
||||
|
||||
def hair_phrase_from_parts(color_key: str, length_key: str, style_key: str) -> str:
|
||||
color = hair_color_text(color_key)
|
||||
length = hair_length_text(length_key)
|
||||
prefix = " ".join(part for part in (length, color) if part)
|
||||
if style_key == "pixie_cut":
|
||||
return f"short {color} pixie cut"
|
||||
if style_key == "bob":
|
||||
return f"{color} bob" if length_key in ("random", "bob_lob", "short") else f"{prefix} bob"
|
||||
if style_key == "lob":
|
||||
return f"shoulder-length {color} lob" if length_key in ("random", "bob_lob") else f"{prefix} lob"
|
||||
if style_key == "shag":
|
||||
return f"{prefix or color} shag"
|
||||
if style_key == "ponytail":
|
||||
return f"{prefix or color} ponytail"
|
||||
if style_key == "braid":
|
||||
return f"{prefix or color} braid"
|
||||
if style_key == "braids":
|
||||
return f"{prefix or color} braids"
|
||||
if style_key == "bun":
|
||||
return f"{prefix} hair in a bun" if length else f"{color} bun"
|
||||
if style_key == "messy_bun":
|
||||
return f"{prefix} hair in a messy bun" if length else f"messy {color} bun"
|
||||
if style_key == "locs":
|
||||
return f"{prefix or color} locs"
|
||||
if style_key == "twists":
|
||||
return f"{prefix or color} twists"
|
||||
if style_key == "afro":
|
||||
return f"{color} afro"
|
||||
if style_key == "natural_curls":
|
||||
return f"{prefix or color} natural curls"
|
||||
if style_key == "wet_hair":
|
||||
return f"{prefix or color} wet hair"
|
||||
if style_key == "slicked_back":
|
||||
return f"slicked-back {color} hair"
|
||||
if style_key == "straight":
|
||||
return f"{prefix or color} straight hair"
|
||||
if style_key == "loose_waves":
|
||||
return f"{prefix or color} loose waves"
|
||||
if style_key == "tight_curls":
|
||||
return f"{prefix or color} tight curls"
|
||||
if style_key == "curls":
|
||||
return f"{prefix or color} curls"
|
||||
return f"{prefix or color} waves"
|
||||
|
||||
|
||||
_slot_value = slot_value
|
||||
_normalize_descriptor_detail = normalize_descriptor_detail
|
||||
_normalize_presence_mode = normalize_presence_mode
|
||||
_normalize_slot_seed = normalize_slot_seed
|
||||
_character_figure_choices = character_figure_choices
|
||||
_empty_characteristics_config = empty_characteristics_config
|
||||
_normalize_characteristic_choice = normalize_characteristic_choice
|
||||
_normalize_characteristic_values = normalize_characteristic_values
|
||||
_parse_characteristics_config = parse_characteristics_config
|
||||
_characteristics_summary = characteristics_summary
|
||||
_characteristic_choice = characteristic_choice
|
||||
_eye_phrase_from_key = eye_phrase_from_key
|
||||
_normalize_hair_choice = normalize_hair_choice
|
||||
_infer_hair_color_key = infer_hair_color_key
|
||||
_infer_hair_length_key = infer_hair_length_key
|
||||
_infer_hair_style_key = infer_hair_style_key
|
||||
_choose_hair_key = choose_hair_key
|
||||
_normalize_hair_values = normalize_hair_values
|
||||
_empty_hair_config = empty_hair_config
|
||||
_parse_hair_config = parse_hair_config
|
||||
_hair_config_summary = hair_config_summary
|
||||
_hair_color_text = hair_color_text
|
||||
_hair_length_text = hair_length_text
|
||||
_hair_phrase_from_parts = hair_phrase_from_parts
|
||||
@@ -0,0 +1,480 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from . import character_config as character_policy
|
||||
except ImportError: # Allows local smoke tests from the repository root.
|
||||
import character_config as character_policy
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent
|
||||
PROFILE_DIR = ROOT_DIR / "profiles"
|
||||
CHARACTER_MANUAL_FIELDS = (
|
||||
"manual_age",
|
||||
"manual_body",
|
||||
"body_phrase",
|
||||
"skin",
|
||||
"hair",
|
||||
"eyes",
|
||||
"softcore_outfit",
|
||||
"hardcore_clothing",
|
||||
)
|
||||
|
||||
|
||||
def body_phrase(body: Any, figure_note: Any = "") -> str:
|
||||
body = str(body or "").strip()
|
||||
figure_note = str(figure_note or "").strip()
|
||||
if not body:
|
||||
return figure_note
|
||||
if not figure_note:
|
||||
return f"{body} figure"
|
||||
if "figure" in figure_note.lower():
|
||||
return f"{body} build and {figure_note}"
|
||||
return f"{body} figure with {figure_note}"
|
||||
|
||||
|
||||
def safe_profile_name(profile_name: str) -> str:
|
||||
profile_name = re.sub(r"[^a-zA-Z0-9_-]+", "_", str(profile_name or "").strip()).strip("_")
|
||||
return profile_name[:64] or "profile"
|
||||
|
||||
|
||||
def profile_path(profile_name: str) -> Path:
|
||||
return PROFILE_DIR / f"{safe_profile_name(profile_name)}.json"
|
||||
|
||||
|
||||
def character_profile_choices() -> list[str]:
|
||||
if not PROFILE_DIR.exists():
|
||||
return ["manual"]
|
||||
names = sorted(path.stem for path in PROFILE_DIR.glob("*.json") if path.is_file())
|
||||
return ["manual"] + names
|
||||
|
||||
|
||||
def load_json_object(value: str | dict[str, Any] | None, label: str) -> dict[str, Any]:
|
||||
if not value:
|
||||
return {}
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
try:
|
||||
raw = json.loads(str(value))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid {label} JSON: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError(f"{label} must be a JSON object.")
|
||||
return raw
|
||||
|
||||
|
||||
def parse_character_manual_config(value: str | dict[str, Any] | None) -> dict[str, str]:
|
||||
if not value:
|
||||
return {}
|
||||
if isinstance(value, dict):
|
||||
raw = value
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(value))
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
if not isinstance(raw, dict):
|
||||
return {}
|
||||
return {
|
||||
key: str(raw.get(key) or "").strip()
|
||||
for key in CHARACTER_MANUAL_FIELDS
|
||||
if str(raw.get(key) or "").strip()
|
||||
}
|
||||
|
||||
|
||||
def character_manual_summary(config: dict[str, str]) -> str:
|
||||
parts = [f"{key}={value}" for key, value in config.items() if value]
|
||||
return "; ".join(parts) if parts else "manual unrestricted"
|
||||
|
||||
|
||||
def build_character_manual_config_json(
|
||||
manual: str | dict[str, Any] | None = "",
|
||||
combine_mode: str = "merge_nonempty",
|
||||
manual_age: str = "",
|
||||
manual_body: str = "",
|
||||
body_phrase: str = "",
|
||||
skin: str = "",
|
||||
hair: str = "",
|
||||
eyes: str = "",
|
||||
softcore_outfit: str = "",
|
||||
hardcore_clothing: str = "",
|
||||
) -> str:
|
||||
base = {} if combine_mode == "replace_all" else parse_character_manual_config(manual)
|
||||
updates = {
|
||||
"manual_age": manual_age,
|
||||
"manual_body": manual_body,
|
||||
"body_phrase": body_phrase,
|
||||
"skin": skin,
|
||||
"hair": hair,
|
||||
"eyes": eyes,
|
||||
"softcore_outfit": softcore_outfit,
|
||||
"hardcore_clothing": hardcore_clothing,
|
||||
}
|
||||
for key, value in updates.items():
|
||||
value = str(value or "").strip()
|
||||
if value:
|
||||
base[key] = value
|
||||
result = {"config_type": "character_manual", **base}
|
||||
result["summary"] = character_manual_summary(base)
|
||||
return json.dumps(result, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str:
|
||||
detail = character_policy.normalize_descriptor_detail(descriptor_detail)
|
||||
if detail != "auto":
|
||||
return detail
|
||||
return "compact" if str(subject or "").strip().lower() == "man" else "full"
|
||||
|
||||
|
||||
def descriptor_from_parts(
|
||||
subject: Any,
|
||||
age: Any,
|
||||
body_phrase_value: Any,
|
||||
skin: Any,
|
||||
hair: Any,
|
||||
eyes: Any,
|
||||
descriptor_detail: Any = "auto",
|
||||
) -> str:
|
||||
subject = str(subject or "person").strip() or "person"
|
||||
age_text = " ".join(str(age or "").strip().split())
|
||||
age_text = age_text.removesuffix(" adults").removesuffix(" adult").strip()
|
||||
if age_text in ("adult", "adults"):
|
||||
age_text = ""
|
||||
subject_phrase = f"{age_text} adult {subject}".strip() if age_text else f"adult {subject}"
|
||||
detail = descriptor_detail_for_subject(subject, descriptor_detail)
|
||||
detail_map = {
|
||||
"minimal": (body_phrase_value,),
|
||||
"compact": (body_phrase_value, skin),
|
||||
"medium": (body_phrase_value, skin, hair),
|
||||
"full": (body_phrase_value, skin, hair, eyes),
|
||||
}
|
||||
pieces = [subject_phrase, *detail_map.get(detail, detail_map["full"])]
|
||||
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
|
||||
|
||||
|
||||
def row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
row = load_json_object(metadata_json, "metadata_json")
|
||||
if isinstance(row.get("softcore_row"), dict):
|
||||
return row["softcore_row"]
|
||||
return row
|
||||
|
||||
|
||||
def character_profile_descriptor(profile: dict[str, Any]) -> str:
|
||||
subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip()
|
||||
return descriptor_from_parts(
|
||||
subject,
|
||||
profile.get("age"),
|
||||
profile.get("body_phrase") or body_phrase(profile.get("body"), profile.get("figure")),
|
||||
profile.get("skin"),
|
||||
profile.get("hair"),
|
||||
profile.get("eyes"),
|
||||
profile.get("descriptor_detail"),
|
||||
)
|
||||
|
||||
|
||||
def normalize_character_profile(profile: dict[str, Any], profile_name: str = "") -> dict[str, Any]:
|
||||
subject_type = str(profile.get("subject_type") or profile.get("primary_subject") or profile.get("subject") or "").strip()
|
||||
if subject_type not in ("woman", "man"):
|
||||
subject_type = "woman"
|
||||
body = str(profile.get("body") or profile.get("body_type") or "").strip()
|
||||
figure = str(profile.get("figure") or "").strip()
|
||||
normalized_body_phrase = str(profile.get("body_phrase") or "").strip() or body_phrase(body, figure)
|
||||
normalized = {
|
||||
"profile_type": "character",
|
||||
"profile_name": safe_profile_name(profile_name or str(profile.get("profile_name") or "")),
|
||||
"subject_type": subject_type,
|
||||
"subject": subject_type,
|
||||
"subject_phrase": subject_type,
|
||||
"age": str(profile.get("age") or profile.get("age_band") or "").strip(),
|
||||
"body": body,
|
||||
"body_phrase": normalized_body_phrase,
|
||||
"skin": str(profile.get("skin") or "").strip(),
|
||||
"hair": str(profile.get("hair") or "").strip(),
|
||||
"eyes": str(profile.get("eyes") or "").strip(),
|
||||
"figure": figure,
|
||||
"descriptor_detail": character_policy.normalize_descriptor_detail(profile.get("descriptor_detail")),
|
||||
}
|
||||
normalized["descriptor"] = character_profile_descriptor(normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def build_character_profile_json(
|
||||
profile_name: str = "",
|
||||
source: str = "metadata_json",
|
||||
metadata_json: str | dict[str, Any] | None = "",
|
||||
character_slot_row: dict[str, Any] | None = None,
|
||||
subject_type: str = "woman",
|
||||
age: str = "",
|
||||
body: str = "",
|
||||
body_phrase_value: str = "",
|
||||
skin: str = "",
|
||||
hair: str = "",
|
||||
eyes: str = "",
|
||||
figure: str = "",
|
||||
save_now: bool = False,
|
||||
) -> dict[str, str]:
|
||||
if source == "character_slot":
|
||||
row = character_slot_row or {}
|
||||
raw_profile = {
|
||||
"profile_name": profile_name,
|
||||
"subject_type": row.get("subject_type") or subject_type,
|
||||
"age": row.get("age") or age,
|
||||
"body": row.get("body") or body,
|
||||
"body_phrase": row.get("body_phrase") or body_phrase_value,
|
||||
"skin": row.get("skin") or skin,
|
||||
"hair": row.get("hair") or hair,
|
||||
"eyes": row.get("eyes") or eyes,
|
||||
"figure": row.get("figure") or figure,
|
||||
"descriptor_detail": row.get("descriptor_detail") or "auto",
|
||||
}
|
||||
elif source == "metadata_json":
|
||||
row = row_from_profile_metadata(metadata_json)
|
||||
raw_profile = {
|
||||
"profile_name": profile_name,
|
||||
"subject_type": row.get("subject_type") or row.get("primary_subject") or subject_type,
|
||||
"age": row.get("age") or row.get("age_band") or age,
|
||||
"body": row.get("body") or row.get("body_type") or body,
|
||||
"body_phrase": row.get("body_phrase") or body_phrase_value,
|
||||
"skin": row.get("skin") or skin,
|
||||
"hair": row.get("hair") or hair,
|
||||
"eyes": row.get("eyes") or eyes,
|
||||
"figure": row.get("figure") or figure,
|
||||
"descriptor_detail": row.get("descriptor_detail") or "auto",
|
||||
}
|
||||
else:
|
||||
raw_profile = {
|
||||
"profile_name": profile_name,
|
||||
"subject_type": subject_type,
|
||||
"age": age,
|
||||
"body": body,
|
||||
"body_phrase": body_phrase_value,
|
||||
"skin": skin,
|
||||
"hair": hair,
|
||||
"eyes": eyes,
|
||||
"figure": figure,
|
||||
"descriptor_detail": "auto",
|
||||
}
|
||||
profile = normalize_character_profile(raw_profile, profile_name)
|
||||
saved_path = ""
|
||||
status = "not_saved"
|
||||
if save_now:
|
||||
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
path = profile_path(profile["profile_name"])
|
||||
path.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
saved_path = str(path)
|
||||
status = "saved"
|
||||
return {
|
||||
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
||||
"profile_name": profile["profile_name"],
|
||||
"descriptor": profile["descriptor"],
|
||||
"saved_path": saved_path,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def save_character_profile_payload(profile_name: str = "", profile_json: str | dict[str, Any] | None = "") -> dict[str, str]:
|
||||
raw_profile = load_json_object(profile_json, "profile_json")
|
||||
if not raw_profile:
|
||||
raise ValueError("No cached character profile is available to save.")
|
||||
profile = normalize_character_profile(raw_profile, profile_name or str(raw_profile.get("profile_name") or ""))
|
||||
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
path = profile_path(profile["profile_name"])
|
||||
path.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
return {
|
||||
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
||||
"profile_name": profile["profile_name"],
|
||||
"descriptor": profile["descriptor"],
|
||||
"saved_path": str(path),
|
||||
"status": "saved",
|
||||
}
|
||||
|
||||
|
||||
def empty_profile_result(status: str = "empty") -> dict[str, str]:
|
||||
return {
|
||||
"profile_json": "",
|
||||
"profile_name": "",
|
||||
"descriptor": "",
|
||||
"saved_path": "",
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def apply_character_profile_overrides(
|
||||
profile: dict[str, Any],
|
||||
override_subject_type: str = "",
|
||||
override_age: str = "",
|
||||
override_body: str = "",
|
||||
override_body_phrase: str = "",
|
||||
override_skin: str = "",
|
||||
override_hair: str = "",
|
||||
override_eyes: str = "",
|
||||
override_figure: str = "",
|
||||
override_descriptor_detail: str = "",
|
||||
) -> dict[str, Any]:
|
||||
updated = dict(profile)
|
||||
subject_type = str(override_subject_type or "").strip()
|
||||
if subject_type in ("woman", "man"):
|
||||
updated["subject_type"] = subject_type
|
||||
updated["subject"] = subject_type
|
||||
updated["subject_phrase"] = subject_type
|
||||
for key, value in (
|
||||
("age", override_age),
|
||||
("body", override_body),
|
||||
("body_phrase", override_body_phrase),
|
||||
("skin", override_skin),
|
||||
("hair", override_hair),
|
||||
("eyes", override_eyes),
|
||||
("figure", override_figure),
|
||||
):
|
||||
text = str(value or "").strip()
|
||||
if text:
|
||||
updated[key] = text
|
||||
descriptor_detail = str(override_descriptor_detail or "").strip()
|
||||
if descriptor_detail and descriptor_detail != "keep_profile":
|
||||
updated["descriptor_detail"] = character_policy.normalize_descriptor_detail(descriptor_detail)
|
||||
if not str(updated.get("body_phrase") or "").strip():
|
||||
updated["body_phrase"] = body_phrase(updated.get("body"), updated.get("figure"))
|
||||
updated["descriptor"] = character_profile_descriptor(updated)
|
||||
return updated
|
||||
|
||||
|
||||
def load_character_profile_json(
|
||||
profile_name: str = "",
|
||||
fallback_profile_json: str | dict[str, Any] | None = "",
|
||||
enabled: bool = True,
|
||||
delete_now: bool = False,
|
||||
rename_now: bool = False,
|
||||
rename_to: str = "",
|
||||
override_subject_type: str = "",
|
||||
override_age: str = "",
|
||||
override_body: str = "",
|
||||
override_body_phrase: str = "",
|
||||
override_skin: str = "",
|
||||
override_hair: str = "",
|
||||
override_eyes: str = "",
|
||||
override_figure: str = "",
|
||||
override_descriptor_detail: str = "",
|
||||
) -> dict[str, str]:
|
||||
if not enabled:
|
||||
return empty_profile_result("disabled")
|
||||
if delete_now and rename_now:
|
||||
return empty_profile_result("choose_delete_or_rename")
|
||||
|
||||
raw_profile = load_json_object(fallback_profile_json, "fallback_profile_json")
|
||||
saved_path = ""
|
||||
if profile_name and profile_name != "manual":
|
||||
path = profile_path(profile_name)
|
||||
if delete_now:
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
return empty_profile_result(f"deleted:{path.stem}")
|
||||
return empty_profile_result(f"delete_missing:{safe_profile_name(profile_name)}")
|
||||
if rename_now:
|
||||
new_name = safe_profile_name(rename_to)
|
||||
if not rename_to.strip():
|
||||
return empty_profile_result("rename_missing_name")
|
||||
if not path.exists():
|
||||
return empty_profile_result(f"rename_missing:{safe_profile_name(profile_name)}")
|
||||
target = profile_path(new_name)
|
||||
if target.exists() and target != path:
|
||||
return empty_profile_result(f"rename_target_exists:{target.stem}")
|
||||
raw_profile = load_json_object(path.read_text(encoding="utf-8"), "character_profile")
|
||||
profile = normalize_character_profile(raw_profile, new_name)
|
||||
target.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
if target != path:
|
||||
path.unlink()
|
||||
return {
|
||||
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
||||
"profile_name": profile["profile_name"],
|
||||
"descriptor": profile["descriptor"],
|
||||
"saved_path": str(target),
|
||||
"status": f"renamed:{path.stem}->{target.stem}",
|
||||
}
|
||||
if path.exists():
|
||||
raw_profile = load_json_object(path.read_text(encoding="utf-8"), "character_profile")
|
||||
saved_path = str(path)
|
||||
if not raw_profile:
|
||||
return empty_profile_result("empty")
|
||||
profile = normalize_character_profile(raw_profile, profile_name or raw_profile.get("profile_name", ""))
|
||||
profile = apply_character_profile_overrides(
|
||||
profile,
|
||||
override_subject_type=override_subject_type,
|
||||
override_age=override_age,
|
||||
override_body=override_body,
|
||||
override_body_phrase=override_body_phrase,
|
||||
override_skin=override_skin,
|
||||
override_hair=override_hair,
|
||||
override_eyes=override_eyes,
|
||||
override_figure=override_figure,
|
||||
override_descriptor_detail=override_descriptor_detail,
|
||||
)
|
||||
return {
|
||||
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
||||
"profile_name": profile["profile_name"],
|
||||
"descriptor": profile["descriptor"],
|
||||
"saved_path": saved_path,
|
||||
"status": "loaded" if saved_path else "fallback",
|
||||
}
|
||||
|
||||
|
||||
def parse_character_profile(character_profile: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
raw = load_json_object(character_profile, "character_profile")
|
||||
if not raw:
|
||||
return {}
|
||||
if raw.get("profile_type") == "character" or any(key in raw for key in ("age", "age_band", "skin", "hair", "eyes")):
|
||||
return normalize_character_profile(raw, str(raw.get("profile_name") or ""))
|
||||
return {}
|
||||
|
||||
|
||||
def apply_character_profile_to_context(
|
||||
context: dict[str, Any],
|
||||
character_profile: str | dict[str, Any] | None,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], str]:
|
||||
profile = parse_character_profile(character_profile)
|
||||
if not profile:
|
||||
return context, {}, "none"
|
||||
if context.get("subject_type") not in ("woman", "man"):
|
||||
return context, profile, "skipped_non_single_subject"
|
||||
if profile["subject_type"] != context.get("subject_type"):
|
||||
return context, profile, "skipped_subject_mismatch"
|
||||
updated = dict(context)
|
||||
for key in (
|
||||
"subject_type",
|
||||
"subject",
|
||||
"subject_phrase",
|
||||
"age",
|
||||
"body",
|
||||
"body_phrase",
|
||||
"skin",
|
||||
"hair",
|
||||
"eyes",
|
||||
"figure",
|
||||
"descriptor_detail",
|
||||
):
|
||||
value = profile.get(key)
|
||||
if value:
|
||||
updated[key] = value
|
||||
updated["subject"] = profile["subject_type"]
|
||||
updated["subject_phrase"] = profile["subject_type"]
|
||||
return updated, profile, "applied"
|
||||
|
||||
|
||||
_body_phrase = body_phrase
|
||||
_safe_profile_name = safe_profile_name
|
||||
_profile_path = profile_path
|
||||
_load_json_object = load_json_object
|
||||
_parse_character_manual_config = parse_character_manual_config
|
||||
_character_manual_summary = character_manual_summary
|
||||
_descriptor_detail_for_subject = descriptor_detail_for_subject
|
||||
_descriptor_from_parts = descriptor_from_parts
|
||||
_row_from_profile_metadata = row_from_profile_metadata
|
||||
_character_profile_descriptor = character_profile_descriptor
|
||||
_normalize_character_profile = normalize_character_profile
|
||||
_empty_profile_result = empty_profile_result
|
||||
_apply_character_profile_overrides = apply_character_profile_overrides
|
||||
_parse_character_profile = parse_character_profile
|
||||
_apply_character_profile_to_context = apply_character_profile_to_context
|
||||
@@ -0,0 +1,355 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from . import character_config as character_policy
|
||||
from . import character_profile as character_profile_policy
|
||||
from . import filter_config as filter_policy
|
||||
from . import pov_policy
|
||||
from . import seed_config as seed_policy
|
||||
except ImportError: # Allows local smoke tests with top-level imports.
|
||||
import character_config as character_policy
|
||||
import character_profile as character_profile_policy
|
||||
import filter_config as filter_policy
|
||||
import pov_policy
|
||||
import seed_config as seed_policy
|
||||
|
||||
|
||||
def _is_false(value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value is False
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ("false", "0", "no", "off")
|
||||
return False
|
||||
|
||||
|
||||
def _clamped_float(value: Any, default: float = 0.5, min_value: float = 0.0, max_value: float = 1.0) -> float:
|
||||
try:
|
||||
number = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return max(min_value, min(max_value, number))
|
||||
|
||||
|
||||
def normalize_slot_expression_intensity(value: Any) -> float:
|
||||
try:
|
||||
intensity = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return -1.0
|
||||
if intensity < 0:
|
||||
return -1.0
|
||||
return _clamped_float(intensity, 0.5)
|
||||
|
||||
|
||||
def slot_expression_enabled(slot: dict[str, Any] | None) -> bool:
|
||||
if not slot:
|
||||
return True
|
||||
return not _is_false(slot.get("expression_enabled", True))
|
||||
|
||||
|
||||
def slot_expression_intensity(slot: dict[str, Any] | None) -> float | None:
|
||||
if not slot or not slot_expression_enabled(slot):
|
||||
return None
|
||||
intensity = normalize_slot_expression_intensity(slot.get("expression_intensity"))
|
||||
return intensity if intensity >= 0 else None
|
||||
|
||||
|
||||
def slot_expression_intensity_for_phase(slot: dict[str, Any] | None, phase: str = "") -> float | None:
|
||||
if not slot or not slot_expression_enabled(slot):
|
||||
return None
|
||||
phase_key = f"{phase}_expression_intensity" if phase in ("softcore", "hardcore") else ""
|
||||
if phase_key:
|
||||
intensity = normalize_slot_expression_intensity(slot.get(phase_key))
|
||||
if intensity >= 0:
|
||||
return intensity
|
||||
return slot_expression_intensity(slot)
|
||||
|
||||
|
||||
def normalize_slot_seed(value: Any) -> int:
|
||||
return character_policy.normalize_slot_seed(value)
|
||||
|
||||
|
||||
def slot_seed(slot: dict[str, Any] | None) -> int:
|
||||
if not slot:
|
||||
return -1
|
||||
return normalize_slot_seed(slot.get("slot_seed"))
|
||||
|
||||
|
||||
def slot_seeded_rng(slot: dict[str, Any] | None, salt: int) -> random.Random | None:
|
||||
seed = slot_seed(slot)
|
||||
if seed < 0:
|
||||
return None
|
||||
return random.Random(seed_policy.row_seed(seed, 1, salt))
|
||||
|
||||
|
||||
def slot_context_rng(slot: dict[str, Any], fallback_rng: random.Random) -> random.Random:
|
||||
return slot_seeded_rng(slot, 701) or fallback_rng
|
||||
|
||||
|
||||
def slot_effective_figure(
|
||||
slot: dict[str, Any],
|
||||
subject_type: str,
|
||||
fallback_figure: str,
|
||||
) -> str:
|
||||
raw_figure = str(slot.get("figure") or "random").strip()
|
||||
if raw_figure in ("curvy", "balanced", "bombshell"):
|
||||
return raw_figure
|
||||
seeded_rng = slot_seeded_rng(slot, 709)
|
||||
if subject_type == "woman" and seeded_rng is not None:
|
||||
options = ["curvy", "balanced", "bombshell"]
|
||||
return options[seeded_rng.randrange(len(options))]
|
||||
return fallback_figure
|
||||
|
||||
|
||||
def slot_manual_or_choice(choice: str, manual_value: str) -> str:
|
||||
choice = str(choice or "").strip()
|
||||
manual_value = str(manual_value or "").strip()
|
||||
if choice == "manual":
|
||||
return manual_value or "random"
|
||||
if choice.lower() in character_policy.CHARACTER_RANDOM_TOKENS:
|
||||
return "random"
|
||||
return choice
|
||||
|
||||
|
||||
def normalize_slot_ethnicity(value: Any) -> str:
|
||||
return filter_policy.normalize_ethnicity_filter(value, "random", allow_random=True)
|
||||
|
||||
|
||||
def normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
|
||||
subject_type = str(slot.get("subject_type") or slot.get("subject") or "").strip().lower()
|
||||
if subject_type not in ("woman", "man"):
|
||||
subject_type = "woman"
|
||||
label = str(slot.get("label") or slot.get("label_mode") or "auto_chain").strip()
|
||||
label = label.replace("Woman ", "").replace("Man ", "").strip().upper()
|
||||
if label == "AUTO_CHAIN":
|
||||
label = "auto_chain"
|
||||
if label not in character_policy.CHARACTER_LABEL_CHOICES:
|
||||
label = "auto_chain"
|
||||
|
||||
manual_config = character_profile_policy.parse_character_manual_config(slot.get("manual") or slot.get("manual_config"))
|
||||
|
||||
raw_age = str(slot.get("age") or "random")
|
||||
raw_manual_age = str(slot.get("manual_age") or "").strip()
|
||||
if not raw_manual_age and manual_config.get("manual_age"):
|
||||
raw_manual_age = manual_config["manual_age"]
|
||||
if raw_age.lower() in character_policy.CHARACTER_RANDOM_TOKENS:
|
||||
raw_age = "manual"
|
||||
age = slot_manual_or_choice(raw_age, raw_manual_age)
|
||||
|
||||
raw_body = str(slot.get("body") or "random")
|
||||
raw_manual_body = str(slot.get("manual_body") or "").strip()
|
||||
if not raw_manual_body and manual_config.get("manual_body"):
|
||||
raw_manual_body = manual_config["manual_body"]
|
||||
if raw_body.lower() in character_policy.CHARACTER_RANDOM_TOKENS:
|
||||
raw_body = "manual"
|
||||
body = slot_manual_or_choice(raw_body, raw_manual_body)
|
||||
figure = str(slot.get("figure") or "random").strip()
|
||||
if figure not in character_policy.CHARACTER_FIGURE_CHOICES:
|
||||
figure = "random"
|
||||
|
||||
def manual_fallback(field: str) -> str:
|
||||
direct = character_policy.slot_value(slot.get(field))
|
||||
return direct or manual_config.get(field, "")
|
||||
|
||||
normalized = {
|
||||
"profile_type": "character_slot",
|
||||
"subject_type": subject_type,
|
||||
"label": label,
|
||||
"slot_seed": normalize_slot_seed(slot.get("slot_seed")),
|
||||
"age": age,
|
||||
"ethnicity": normalize_slot_ethnicity(slot.get("ethnicity")),
|
||||
"figure": figure,
|
||||
"body": body,
|
||||
"body_phrase": manual_fallback("body_phrase"),
|
||||
"skin": manual_fallback("skin"),
|
||||
"hair": manual_fallback("hair"),
|
||||
"manual": manual_config,
|
||||
"characteristics": (
|
||||
slot.get("characteristics")
|
||||
if isinstance(slot.get("characteristics"), dict)
|
||||
else character_policy.slot_value(slot.get("characteristics") or slot.get("characteristics_config"))
|
||||
),
|
||||
"hair_config": (
|
||||
slot.get("hair_config")
|
||||
if isinstance(slot.get("hair_config"), dict)
|
||||
else character_policy.slot_value(slot.get("hair_config"))
|
||||
),
|
||||
"hair_color": character_policy.normalize_hair_choice(slot.get("hair_color"), character_policy.CHARACTER_HAIR_COLOR_CHOICES),
|
||||
"hair_length": character_policy.normalize_hair_choice(
|
||||
slot.get("hair_length"),
|
||||
character_policy.CHARACTER_HAIR_LENGTH_CHOICES,
|
||||
),
|
||||
"hair_style": character_policy.normalize_hair_choice(slot.get("hair_style"), character_policy.CHARACTER_HAIR_STYLE_CHOICES),
|
||||
"eyes": manual_fallback("eyes"),
|
||||
"descriptor_detail": character_policy.normalize_descriptor_detail(slot.get("descriptor_detail")),
|
||||
"presence_mode": character_policy.normalize_presence_mode(slot.get("presence_mode"), subject_type),
|
||||
"softcore_outfit": manual_fallback("softcore_outfit"),
|
||||
"hardcore_clothing": (
|
||||
character_policy.slot_value(slot.get("hardcore_clothing") or slot.get("hardcore_outfit"))
|
||||
or manual_config.get("hardcore_clothing", "")
|
||||
),
|
||||
"expression_enabled": not _is_false(slot.get("expression_enabled", True)),
|
||||
"expression_intensity": normalize_slot_expression_intensity(slot.get("expression_intensity")),
|
||||
"softcore_expression_intensity": normalize_slot_expression_intensity(slot.get("softcore_expression_intensity")),
|
||||
"hardcore_expression_intensity": normalize_slot_expression_intensity(slot.get("hardcore_expression_intensity")),
|
||||
}
|
||||
normalized["summary"] = character_slot_summary(normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def parse_character_cast(character_cast: str | dict[str, Any] | list[Any] | None) -> list[dict[str, Any]]:
|
||||
if not character_cast:
|
||||
return []
|
||||
if isinstance(character_cast, list):
|
||||
raw = character_cast
|
||||
elif isinstance(character_cast, dict):
|
||||
raw = character_cast
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(character_cast))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid character_cast JSON: {exc}") from exc
|
||||
|
||||
if isinstance(raw, list):
|
||||
slots = raw
|
||||
elif isinstance(raw, dict) and isinstance(raw.get("slots"), list):
|
||||
slots = raw["slots"]
|
||||
elif isinstance(raw, dict) and raw.get("profile_type") == "character_slot":
|
||||
slots = [raw]
|
||||
elif isinstance(raw, dict) and raw.get("subject_type") in ("woman", "man"):
|
||||
slots = [raw]
|
||||
else:
|
||||
return []
|
||||
return [normalize_character_slot(slot) for slot in slots if isinstance(slot, dict)]
|
||||
|
||||
|
||||
def character_slot_summary(slot: dict[str, Any]) -> str:
|
||||
subject = str(slot.get("subject_type") or "woman")
|
||||
label = str(slot.get("label") or "auto_chain")
|
||||
label_text = "nearest free label" if label == "auto_chain" else f"{subject.capitalize()} {label}"
|
||||
parts = [
|
||||
subject,
|
||||
label_text,
|
||||
f"seed={slot.get('slot_seed')}" if slot_seed(slot) >= 0 else "",
|
||||
f"age={slot.get('age', 'random')}",
|
||||
f"ethnicity={slot.get('ethnicity', 'random')}",
|
||||
f"figure={slot.get('figure', 'random')}",
|
||||
f"body={slot.get('body', 'random')}",
|
||||
f"detail={slot.get('descriptor_detail', 'auto')}",
|
||||
]
|
||||
parts = [part for part in parts if part]
|
||||
if pov_policy.slot_is_pov(slot):
|
||||
parts.append("presence=pov")
|
||||
if not slot_expression_enabled(slot):
|
||||
parts.append("expression=disabled")
|
||||
else:
|
||||
expression_intensity = slot_expression_intensity(slot)
|
||||
if expression_intensity is not None:
|
||||
parts.append(f"expression={expression_intensity:.2f}")
|
||||
softcore_expression_intensity = slot_expression_intensity_for_phase(slot, "softcore")
|
||||
hardcore_expression_intensity = slot_expression_intensity_for_phase(slot, "hardcore")
|
||||
if softcore_expression_intensity is not None and softcore_expression_intensity != expression_intensity:
|
||||
parts.append(f"soft_expr={softcore_expression_intensity:.2f}")
|
||||
if hardcore_expression_intensity is not None and hardcore_expression_intensity != expression_intensity:
|
||||
parts.append(f"hard_expr={hardcore_expression_intensity:.2f}")
|
||||
if slot.get("softcore_outfit"):
|
||||
parts.append(f"soft_outfit={slot['softcore_outfit']}")
|
||||
if slot.get("hardcore_clothing"):
|
||||
parts.append(f"hard_clothing={slot['hardcore_clothing']}")
|
||||
characteristics = character_policy.parse_characteristics_config(slot.get("characteristics"))
|
||||
characteristics_summary = character_policy.characteristics_summary(characteristics)
|
||||
if characteristics_summary != "characteristics unrestricted":
|
||||
parts.append(f"characteristics={characteristics_summary}")
|
||||
hair_config = character_policy.parse_hair_config(slot.get("hair_config"))
|
||||
hair_config_summary = character_policy.hair_config_summary(hair_config)
|
||||
if hair_config_summary != "hair unrestricted":
|
||||
parts.append(f"hair={hair_config_summary}")
|
||||
for key in ("hair_color", "hair_length", "hair_style"):
|
||||
value = slot.get(key)
|
||||
if value and value != "random":
|
||||
parts.append(f"{key}={value}")
|
||||
for key in ("body_phrase", "skin", "hair", "eyes"):
|
||||
value = slot.get(key)
|
||||
if value:
|
||||
parts.append(f"{key}={value}")
|
||||
return "; ".join(parts)
|
||||
|
||||
|
||||
def build_character_slot_json(
|
||||
subject_type: str = "woman",
|
||||
label: str = "auto_chain",
|
||||
slot_seed: int = -1,
|
||||
age: str = "random",
|
||||
manual_age: str = "",
|
||||
manual: str | dict[str, Any] | None = "",
|
||||
ethnicity: str = "random",
|
||||
figure: str = "random",
|
||||
body: str = "random",
|
||||
manual_body: str = "",
|
||||
body_phrase: str = "",
|
||||
skin: str = "",
|
||||
hair: str = "",
|
||||
characteristics: str | dict[str, Any] | None = "",
|
||||
hair_config: str | dict[str, Any] | None = "",
|
||||
hair_color: str = "random",
|
||||
hair_length: str = "random",
|
||||
hair_style: str = "random",
|
||||
eyes: str = "",
|
||||
descriptor_detail: str = "auto",
|
||||
expression_enabled: bool = True,
|
||||
expression_intensity: float = -1.0,
|
||||
enabled: bool = True,
|
||||
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
||||
presence_mode: str = "visible",
|
||||
softcore_expression_intensity: float = -1.0,
|
||||
hardcore_expression_intensity: float = -1.0,
|
||||
softcore_outfit: str = "",
|
||||
hardcore_clothing: str = "",
|
||||
) -> dict[str, str]:
|
||||
existing_slots = parse_character_cast(character_cast)
|
||||
slot = normalize_character_slot(
|
||||
{
|
||||
"subject_type": subject_type,
|
||||
"label": label,
|
||||
"slot_seed": slot_seed,
|
||||
"age": age,
|
||||
"manual_age": manual_age,
|
||||
"manual": manual,
|
||||
"ethnicity": ethnicity,
|
||||
"figure": figure,
|
||||
"body": body,
|
||||
"manual_body": manual_body,
|
||||
"body_phrase": body_phrase,
|
||||
"skin": skin,
|
||||
"hair": hair,
|
||||
"characteristics": characteristics,
|
||||
"hair_config": hair_config,
|
||||
"hair_color": hair_color,
|
||||
"hair_length": hair_length,
|
||||
"hair_style": hair_style,
|
||||
"eyes": eyes,
|
||||
"descriptor_detail": descriptor_detail,
|
||||
"presence_mode": presence_mode,
|
||||
"softcore_outfit": softcore_outfit,
|
||||
"hardcore_clothing": hardcore_clothing,
|
||||
"expression_enabled": expression_enabled,
|
||||
"expression_intensity": expression_intensity,
|
||||
"softcore_expression_intensity": softcore_expression_intensity,
|
||||
"hardcore_expression_intensity": hardcore_expression_intensity,
|
||||
}
|
||||
)
|
||||
slots = existing_slots + ([slot] if enabled else [])
|
||||
cast = {
|
||||
"profile_type": "character_cast",
|
||||
"version": 1,
|
||||
"slots": slots,
|
||||
}
|
||||
return {
|
||||
"character_cast": json.dumps(cast, ensure_ascii=True, sort_keys=True),
|
||||
"character_slot": json.dumps(slot, ensure_ascii=True, sort_keys=True) if enabled else "",
|
||||
"summary": slot["summary"] if enabled else "disabled",
|
||||
"status": f"{len(slots)} slot(s)",
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,262 @@
|
||||
# Krea2 POV Pose Atlas
|
||||
|
||||
Local reference root:
|
||||
|
||||
`/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2`
|
||||
|
||||
Use this dataset as the pose-geometry reference for POV prompt tuning. The pose
|
||||
folders contain rendered POV examples; matching `_control` folders contain the
|
||||
solo/control image for the same pose family. Ignore `bg` and `*_bg` folders for
|
||||
pose analysis; they are background plates without people.
|
||||
|
||||
Machine-readable pose variants live in
|
||||
`categories/krea2_pov_pose_variants.json`. That catalog is intentionally smaller
|
||||
than the full atlas: it only contains variants that are proven or useful
|
||||
candidates for fixed-seed Krea2 tuning. Add a variant there when it has a compact
|
||||
geometry summary, cue phrases, avoid phrases, references, and a known generator
|
||||
hook. Code should read it through `krea2_pose_variant_catalog.py` instead of
|
||||
parsing the JSON directly.
|
||||
|
||||
In ComfyUI, use the `SxCP Krea2 Pose Variant` node when you want a workflow to
|
||||
select one catalog variant and emit a compatible `hardcore_position_config` for
|
||||
the existing Position Pool / Action Filter / Insta-OF chain. Pair it with
|
||||
`SxCP Krea2 Variant Evidence` to display the fixed-seed eval entry, image paths,
|
||||
and generator decision behind that variant.
|
||||
|
||||
For command-line planning, `python tools/krea2_tuning_report.py` shows which
|
||||
catalog variants are proven or pending and which atlas pose folders are still
|
||||
unmapped by the catalog. Unmapped folders include sample pose/control image
|
||||
paths and a suggested candidate key to start the next catalog entry.
|
||||
|
||||
The `ready` folder name is misleading for prompt planning: it is mapped as
|
||||
`pov_ejaculation_aftermath_open_thigh_candidate`, a post-ejaculation
|
||||
open-thigh display family with thick visible fluid around the exposed opening,
|
||||
not as a neutral setup pose.
|
||||
|
||||
## Inventory
|
||||
|
||||
| Family | Pose images | Control images | First sample |
|
||||
| --- | ---: | ---: | --- |
|
||||
| cowgirl | 63 | 63 | `5.cowgirl/100_cowgirl.png` |
|
||||
| cowgirl alt | 62 | 62 | `5.cowgirl_alt/101_cowgirl_alt.png` |
|
||||
| reverse cowgirl | 58 | 58 | `cowgirl_reverse/101_cowgirl_reverse.png` |
|
||||
| reverse cowgirl alt | 50 | 50 | `cowgirl_reversere_alt/100_cowgirl_reversere_alt.png` |
|
||||
| doggy | 57 | 57 | `doggy/101_doggy.png` |
|
||||
| doggy alt | 45 | 45 | `doggy_alt/100_doggy_alt.png` |
|
||||
| missionary | 74 | 74 | `missionary/101_missionary.png` |
|
||||
| missionary folded | 12 | 12 | `missionary_folded/16_missionary_folded.png` |
|
||||
| sixty-nine | 29 | 29 | `69/105_sixtynine.png` |
|
||||
| ballsucking | 25 | 25 | `ballsucking/101_ballsucking.png` |
|
||||
| blowjob laying | 42 | 42 | `blowjob_laying/101_blowjob_laying.png` |
|
||||
| blowjob side | 17 | 17 | `blowjob_side/103_blowjob_side.png` |
|
||||
| blowjob sitting | 27 | 27 | `blowjob_sitting/100_blowjob_sitting.png` |
|
||||
| blowjob top view | 17 | 17 | `blowjob_top_view/102_blowjob_top_view.png` |
|
||||
| boobjob | 11 | 11 | `boobjob/100_boobjob.png` |
|
||||
| handjob | 24 | 24 | `handjob/18_handjob.png` |
|
||||
| footjob | 2 | 2 | `footjob/59_footjob.png` |
|
||||
| fingering | 10 | 10 | `fingering/103_fingering.png` |
|
||||
| spread | 55 | 55 | `spread/100_spread_.png` |
|
||||
| ready | 19 | 19 | `ready/105_ready_.png` |
|
||||
| wand | 7 | 7 | `wand/106_wand_.png` |
|
||||
|
||||
## Tuning Method
|
||||
|
||||
For each pose family:
|
||||
|
||||
1. Sample 5-10 pose images and 2-3 control images.
|
||||
2. Write a compact geometry summary using only repeated visual facts.
|
||||
3. Test one prompt variant with a fixed seed.
|
||||
4. Test the same wording on a second seed or character.
|
||||
5. Patch generator defaults only when the wording improvement repeats or the
|
||||
generated prompt is structurally wrong before rendering.
|
||||
6. Record the evidence in `docs/krea2-prompt-guide.md`.
|
||||
|
||||
## Confirmed Notes
|
||||
|
||||
### Doggy / Rear-Entry
|
||||
|
||||
Dataset references show that visible POV thighs, lower torso, or pelvis can be
|
||||
correct. They should be treated as natural foreground cues, not automatic
|
||||
failures.
|
||||
|
||||
Better Krea2 wording:
|
||||
|
||||
- `top-down POV doggy position from behind`
|
||||
- `camera looks down over the viewer's hands onto the woman's raised hips`
|
||||
- `woman is on all fours with chest low, forearms folded, cheek turned sideways`
|
||||
- `back arched, hips raised high toward the camera`
|
||||
- `viewer hands hold her hips with natural lower-body POV cues in the foreground`
|
||||
|
||||
Avoid using visible shoes or lower legs as the standing cue. In seed `65`, that
|
||||
wording pulled Krea2 toward oral contact and weakened rear-entry geometry.
|
||||
|
||||
### Boobjob / Titjob
|
||||
|
||||
The boobjob folder shows a repeated upright, frontal geometry rather than a
|
||||
forward-bent one: the woman faces the viewer between his thighs, breasts pressed
|
||||
together around a vertical shaft, with the glans above the cleavage near her
|
||||
mouth. For Krea2, name hand ownership when hands matter. In POV prompts, generic
|
||||
`hands` can become the viewer's hands.
|
||||
|
||||
### Handjob
|
||||
|
||||
The handjob folder repeats a centered first-person layout: the viewer's thighs
|
||||
frame the lower edges, the woman faces the viewer between his legs, and her hand
|
||||
is the contact anchor on the shaft. Prompt the woman's hand ownership directly;
|
||||
viewer hands should not cover the action unless that is the intended variant.
|
||||
|
||||
## Candidate Notes
|
||||
|
||||
### Footjob
|
||||
|
||||
The footjob folder is small but visually consistent: the viewer reclines with
|
||||
thighs framing the lower foreground, the penis is upright near the center, and
|
||||
the woman's soles/toes are the contact anchor while her body and face remain
|
||||
behind the feet. Treat `pov_footjob_frontal_sole_stroke` as a candidate until it
|
||||
has fixed-seed Krea2 evidence.
|
||||
|
||||
### Fingering
|
||||
|
||||
The fingering folder repeats a first-person manual-contact layout: the woman is
|
||||
reclined or sitting back with thighs spread wide toward camera, her face and
|
||||
torso visible behind the open-leg frame, and the viewer hand entering from the
|
||||
foreground as the contact anchor. Treat `pov_fingering_reclined_open_thighs` as
|
||||
a candidate until it has fixed-seed Krea2 evidence.
|
||||
|
||||
### Wand / Toy Contact
|
||||
|
||||
The wand folder repeats a close first-person tool-contact layout: the woman is
|
||||
reclined or sitting back with thighs spread toward camera, face and torso visible
|
||||
behind the open-leg frame, and the viewer hand holding a wand-style toy from the
|
||||
foreground with the rounded head pressed to the central contact point. Treat
|
||||
`pov_wand_foreground_tool_contact` as a candidate until it has fixed-seed Krea2
|
||||
evidence. Keep the visible hand/handle in the wording; otherwise Krea2 may float
|
||||
the toy or transfer ownership to the visible partner.
|
||||
|
||||
### Ready / Post-Ejaculation Open-Thigh Display
|
||||
|
||||
The ready folder is not a neutral setup family. It repeats a first-person
|
||||
post-ejaculation display pose: the woman reclines or sits back facing the viewer
|
||||
with thighs spread open, face and torso readable behind the open-leg frame, a
|
||||
viewer body cue or recently withdrawn foreground cue near the lower edge, and
|
||||
thick semen or fluid visible around the exposed pussy or anal opening. Treat
|
||||
`pov_ejaculation_aftermath_open_thigh_candidate` as a candidate until it has
|
||||
fixed-seed Krea2 evidence. Avoid active thrusting wording here; the key state is
|
||||
post-ejaculation fluid visibility, not penetration-in-progress.
|
||||
|
||||
### Spread / Open-Thigh Presentation
|
||||
|
||||
The spread folder is a setup/presentation family rather than a required contact
|
||||
action: the woman faces the camera with legs raised or knees held wide, thighs
|
||||
forming a wide V-frame, and her face and torso visible behind the open-leg pose.
|
||||
Treat `pov_spread_open_thigh_presentation` as a candidate until it has
|
||||
fixed-seed Krea2 evidence.
|
||||
|
||||
### Sixty-Nine / Close Reversed POV
|
||||
|
||||
The `69` folder repeats a close first-person mutual-oral layout rather than a
|
||||
wide side-by-side pose: the visible partner is reversed over the viewer, hips
|
||||
closest to camera, head and torso receding away into the upper frame, and the
|
||||
viewer face or mouth anchoring the lower foreground. Treat
|
||||
`pov_sixty_nine_close_reversed_oral` as the hardest and lowest-priority route in
|
||||
the atlas for now. Do not queue it as a normal prompt-only fixed-seed candidate.
|
||||
When exact geometry matters, prefer a pose/control image or a narrower
|
||||
image-guided route; text alone can collapse this into generic oral contact or
|
||||
lose the reversed-over-viewer body arrangement.
|
||||
|
||||
### Blowjob Top View
|
||||
|
||||
The `blowjob_top_view` folder repeats a top-down first-person oral layout: the
|
||||
viewer looks down from chest or pelvis height, viewer torso or thighs sit at the
|
||||
lower edge, the shaft is vertical and centered, and the woman kneels below
|
||||
looking upward with mouth and hand aligned to it. Treat
|
||||
`pov_blowjob_top_down_vertical_shaft` as a candidate until it has fixed-seed
|
||||
Krea2 evidence.
|
||||
|
||||
### Blowjob Side
|
||||
|
||||
The `blowjob_side` folder repeats a side-profile first-person oral layout: the
|
||||
viewer reclines with torso or thighs visible, the woman leans beside the
|
||||
viewer's pelvis from the side, and her side-facing mouth aligns to the shaft
|
||||
near the lower center of the frame. Treat `pov_blowjob_side_profile_oral` as a
|
||||
candidate until it has fixed-seed Krea2 evidence.
|
||||
|
||||
### Blowjob Laying
|
||||
|
||||
The `blowjob_laying` folder repeats a frontal prone first-person oral layout:
|
||||
the viewer reclines with open thighs framing the lower foreground, the woman
|
||||
lies belly-down between the viewer's thighs, and her front-facing mouth and
|
||||
hands align to a shaft rising from the lower center of the frame. Treat
|
||||
`pov_blowjob_laying_frontal_oral` as a candidate until it has fixed-seed Krea2
|
||||
evidence.
|
||||
|
||||
### Blowjob Sitting
|
||||
|
||||
The `blowjob_sitting` folder includes a few top-view outliers, but the named
|
||||
sitting files repeat an upright seated first-person oral layout: the viewer
|
||||
reclines with open thighs framing the lower foreground, the woman sits upright
|
||||
between the viewer's thighs, and her close front-facing mouth aligns to a
|
||||
vertical centered shaft. Treat `pov_blowjob_sitting_upright_oral` as a candidate
|
||||
until it has fixed-seed Krea2 evidence.
|
||||
|
||||
### Missionary / Open-Leg Penetration
|
||||
|
||||
The `missionary` folder repeats a front-facing first-person penetration layout:
|
||||
the woman reclines on her back facing the viewer, her knees open toward the
|
||||
viewer, her face and torso stay visible behind the open-thigh frame, and the
|
||||
viewer is positioned between her legs from the lower foreground. Treat
|
||||
`pov_missionary_open_leg_penetration` as a candidate until it has fixed-seed
|
||||
Krea2 evidence. Keep this separate from `missionary_folded`, where the legs are
|
||||
pressed much higher and need different wording.
|
||||
|
||||
### Missionary Folded / High-Leg Penetration
|
||||
|
||||
The `missionary_folded` folder repeats a high-leg first-person penetration
|
||||
layout: the woman reclines on her back facing the viewer, her knees are folded
|
||||
high toward her chest, feet or ankles sit close to the camera, and the viewer's
|
||||
hands often hold her calves or ankles while the contact line stays below the
|
||||
raised legs. Treat `pov_missionary_folded_high_leg_penetration` as a candidate
|
||||
until it has fixed-seed Krea2 evidence.
|
||||
|
||||
### Cowgirl / Frontal Straddle Penetration
|
||||
|
||||
The `5.cowgirl` folder repeats a frontal woman-on-top first-person layout: the
|
||||
viewer reclines below, the woman straddles the viewer facing him, her torso
|
||||
stays upright above the contact line, and her knees open to either side of the
|
||||
viewer. Treat `pov_cowgirl_frontal_straddle_penetration` as a candidate until it
|
||||
has fixed-seed Krea2 evidence. Keep this separate from the alt and reverse
|
||||
cowgirl folders, which need their own geometry wording.
|
||||
|
||||
### Cowgirl Alt / Low Seated-Squat Penetration
|
||||
|
||||
The `5.cowgirl_alt` folder is still frontal woman-on-top, not reverse cowgirl,
|
||||
but the repeated pose is lower and closer than the main cowgirl folder: the
|
||||
woman faces the viewer in a low seated squat over the viewer's pelvis, knees
|
||||
bent wide close to the camera, with viewer hands often anchoring the underside
|
||||
of her thighs or hips. Treat `pov_cowgirl_alt_low_squat_penetration` as a
|
||||
candidate until it has fixed-seed Krea2 evidence. Keep this separate from the
|
||||
main cowgirl route so Krea2 can choose between upright straddle wording and
|
||||
closer seated-squat wording.
|
||||
|
||||
### Reverse Cowgirl / Back-Facing Straddle Penetration
|
||||
|
||||
The `cowgirl_reverse` folder repeats a woman-on-top first-person layout where
|
||||
the viewer reclines underneath and the woman faces away from the viewer. Her
|
||||
back, hips, and ass are the closest readable body anchors, with her knees or
|
||||
thighs planted to either side of the viewer's hips; her face may turn back over
|
||||
one shoulder. Treat `pov_reverse_cowgirl_back_facing_penetration` as a
|
||||
candidate until it has fixed-seed Krea2 evidence. Keep it separate from doggy:
|
||||
the viewer is underneath her in a back-facing straddle, not kneeling behind her
|
||||
while she is on all fours.
|
||||
|
||||
### Reverse Cowgirl Alt / Upright Back-Facing Straddle
|
||||
|
||||
The `cowgirl_reversere_alt` folder repeats an upright seated reverse-cowgirl
|
||||
layout. The viewer reclines underneath, while the woman sits upright facing
|
||||
away in a back-facing straddle; her back remains vertical and readable above
|
||||
her hips, with viewer hands often holding her hips, thighs, wrists, or hands.
|
||||
Treat `pov_reverse_cowgirl_alt_upright_back_facing_penetration` as a candidate
|
||||
until it has fixed-seed Krea2 evidence. Keep it separate from
|
||||
`pov_reverse_cowgirl_back_facing_penetration`, which can be closer and more
|
||||
hip-cropped; this alt needs wording that preserves the vertical torso and
|
||||
seated woman-on-top posture.
|
||||
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -7,7 +7,8 @@ routing map in `docs/prompt-pool-routing-map.md`.
|
||||
|
||||
The current branch adds two major surfaces:
|
||||
|
||||
- `SxCP Krea2 Resolution Selector` in `__init__.py`, with README notes.
|
||||
- `SxCP Krea2 Resolution Selector` in `node_seed_resolution.py`, with README
|
||||
notes.
|
||||
- Expanded hardcore interaction/manual/action pools in
|
||||
`categories/sexual_poses.json`,
|
||||
`categories/expression_composition_pools.json`, `prompt_builder.py`, and
|
||||
@@ -20,6 +21,31 @@ The map audit currently sees:
|
||||
- 23 expression pools.
|
||||
- 24 composition pools.
|
||||
- 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
|
||||
|
||||
@@ -52,12 +78,65 @@ It should only handle route-agnostic cleanup:
|
||||
- empty field-label removal;
|
||||
- repeated trigger prefix cleanup;
|
||||
- duplicate comma-list item removal;
|
||||
- route-agnostic negative-prompt merge/dedupe;
|
||||
- adjacent duplicate sentence cleanup;
|
||||
- simple dangling connector cleanup.
|
||||
|
||||
It must not make semantic decisions such as sexual action positioning, POV
|
||||
geometry, clothing state, or model-specific tag weighting. Those stay in the
|
||||
route-specific owner.
|
||||
route-specific owner. It also preserves ordinary words such as `composition`
|
||||
inside normal sentences; empty field-label cleanup is limited to standalone
|
||||
labels.
|
||||
|
||||
Formatter input/fallback parsing now has one home:
|
||||
|
||||
- `formatter_input.py`
|
||||
|
||||
It owns route-neutral parsing shared by Krea2, SDXL, and natural-caption
|
||||
routes:
|
||||
|
||||
- input-hint choice lists and normalization for `auto`, `metadata_json`, and
|
||||
route-specific text modes;
|
||||
- whitespace and punctuation normalization before formatter parsing;
|
||||
- JSON row detection from `metadata_json` or source text;
|
||||
- trigger-prefix stripping with route-specific trigger candidate lists;
|
||||
- `Avoid:` positive/negative splitting for fallback text;
|
||||
- the shared prompt field-label inventory and extraction such as `Setting:`,
|
||||
`Sexual scene:`, `Camera control:`, or `Composition:`;
|
||||
- fallback field-label stripping for tag/text routes that need label-free body
|
||||
text;
|
||||
- row-value fallback from metadata fields to labeled prompt text.
|
||||
|
||||
It must not make formatter-style decisions. Krea prose, SDXL tags, and training
|
||||
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:
|
||||
|
||||
- `hardcore_text_cleanup.py`
|
||||
|
||||
It owns environment-anchor normalization used by both prompt generation and
|
||||
Krea formatting, including malformed surface joins and bed/sheet/couch anchors
|
||||
that should become model-neutral body-support language. It must stay
|
||||
route-neutral: no Krea prose, no SDXL tags, and no category selection logic.
|
||||
|
||||
Current integration points:
|
||||
|
||||
@@ -84,30 +163,227 @@ Keep here:
|
||||
|
||||
Move or isolate later:
|
||||
|
||||
- role graph generation for hardcore interaction categories into a dedicated
|
||||
module, for example `hardcore_role_graphs.py`;
|
||||
- camera-scene adapters into `scene_camera_adapters.py`;
|
||||
- category-library loading and inheritance helpers into `category_library.py`.
|
||||
- pair assembly helpers that still live in `prompt_builder.py`.
|
||||
|
||||
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/
|
||||
composition pool loading, cast compatibility filtering, exact subcategory
|
||||
lookup, and inheritance-based pool merging live in `category_library.py`.
|
||||
- JSON `pool_extensions`, legacy pool patching, built-in category choice lists,
|
||||
and category/subcategory UI choices live in `category_extensions.py`.
|
||||
- object-style item-template metadata extraction, action/position family
|
||||
normalization, position-key normalization, and metadata audit errors live in
|
||||
`category_template_metadata.py`.
|
||||
- row item selection, weighted item/pair choice, item-template axis filling,
|
||||
and oral/outercourse/anal axis compatibility filters live in `row_item.py`;
|
||||
`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_route.py` behind `CategoryItemRoute`, covering hardcore
|
||||
position-category filtering, cast-count adjustment, pose-vs-content seed-axis
|
||||
choice, item metadata collection, legacy dict compatibility, and
|
||||
pose-category item sanitizing; `prompt_builder.py` keeps public delegate
|
||||
wrappers.
|
||||
- row prompt/caption template selection, safe formatting, default prompt
|
||||
templates, configured-cast descriptor insertion, and POV directive insertion
|
||||
live in `row_rendering.py`; `prompt_builder.py` keeps compatibility aliases.
|
||||
- row action/position route metadata resolution lives in
|
||||
`row_route_metadata.py` behind `ActionPositionRoute`, covering template
|
||||
metadata precedence, inferred position-key merging, legacy dict
|
||||
compatibility, and source action-family fallback; `prompt_builder.py` keeps
|
||||
public delegate wrappers.
|
||||
- built-in legacy row generation, auto-weighted/auto-full selection, row mode
|
||||
randomization, ratio clamps, and expression-intensity randomization live in
|
||||
`row_generation.py`; `prompt_builder.py` keeps public delegate wrappers.
|
||||
- category/cast route preset schemas, config JSON builders, choice lists, and
|
||||
parsers live in `category_cast_config.py`; `prompt_builder.py` keeps public
|
||||
delegate wrappers for existing nodes and tests.
|
||||
- generation-time cast count phrases, configured-cast context metadata,
|
||||
character-slot label assignment, scene-kind labels, cast-summary wording, and
|
||||
couple count normalization live in `cast_context.py`; `prompt_builder.py`
|
||||
keeps delegate wrappers where existing generation paths still call the old
|
||||
helper names.
|
||||
- row subject-context routing for single, couple, configured-cast, group, and
|
||||
layout subjects lives in `subject_context.py`; it combines appearance policy,
|
||||
cast metadata, and generator subject pools behind one row-facing entry point.
|
||||
- row subject route orchestration, character slot/profile precedence,
|
||||
configured-cast POV labels, visible cast descriptor collection, and
|
||||
descriptor prompt cleanup live in `row_subject_route.py`;
|
||||
`prompt_builder.py` keeps a public delegate wrapper.
|
||||
- ethnicity/filter choices, advanced filter JSON, ethnicity-list JSON, filter
|
||||
parsing, and ethnicity normalization live in `filter_config.py`; character
|
||||
routes and builder filters use `prompt_builder.py` delegate wrappers.
|
||||
- character choice lists, descriptor detail/presence/slot-seed normalization,
|
||||
characteristic-list JSON builders/parsers, eye labels, hair config
|
||||
builders/parsers, and hair phrase helpers live in `character_config.py`;
|
||||
`prompt_builder.py` keeps public delegate wrappers.
|
||||
- character slot JSON construction, character-cast parsing, slot normalization,
|
||||
slot summary text, slot expression override policy, slot seed helpers, and
|
||||
slot figure/ethnicity normalization live in `character_slot.py`;
|
||||
`prompt_builder.py` keeps public delegate wrappers.
|
||||
- generation-time subject appearance selection, normalized-slot context
|
||||
resolution, slot hair/outfit/clothing selection, character-context row
|
||||
application, and character-slot-to-profile-row conversion live in
|
||||
`character_appearance.py`; `prompt_builder.py` keeps public delegate wrappers.
|
||||
- character manual-detail config, profile name/path policy, profile JSON
|
||||
normalization, descriptor assembly, save/load/rename/delete operations,
|
||||
fallback profile loading, and context override application live in
|
||||
`character_profile.py`; `prompt_builder.py` only bridges generated slot rows
|
||||
into profile saves.
|
||||
- generation profile presets, override normalization, trigger policy, and
|
||||
profile config parsing live in `generation_profile_config.py`;
|
||||
`prompt_builder.py` keeps public delegate wrappers.
|
||||
- location/composition config presets, themed location packs, custom
|
||||
location/composition entry parsing, merge behavior, and config parsing live
|
||||
in `location_config.py`; built-in row location/composition config
|
||||
application, source metadata, and prompt/caption rewrites live in
|
||||
`row_location.py`.
|
||||
- row scene/expression/pose/composition pool routing, category inheritance,
|
||||
runtime location/composition pool overrides, and generator fallback pool
|
||||
selection live in `row_pools.py`; `prompt_builder.py` keeps public delegate
|
||||
wrappers.
|
||||
- row scene/pose/expression/composition axis selection lives in
|
||||
`row_prompt_axes.py` behind `PromptAxesRoute`, covering compatible-entry
|
||||
filtering, expression-disabled handling, per-character expression promotion,
|
||||
legacy dict compatibility, POV composition adaptation, and pose-category
|
||||
environment sanitizing; `prompt_builder.py` keeps public delegate wrappers.
|
||||
- row prompt/caption text-field resolution, prompt/caption template selection,
|
||||
safe formatting, configured-cast descriptor insertion, and POV directive
|
||||
insertion live in `row_rendering.py`; `prompt_builder.py` keeps public
|
||||
delegate wrappers.
|
||||
- row role-graph route sequencing lives in `row_role_graph.py`, covering
|
||||
hardcore source role graph construction, pose-category environment-anchor
|
||||
cleanup, and POV role-graph rewriting before prompt axes and formatter
|
||||
metadata consume the graph.
|
||||
- row expression text cleanup, expression route resolution, expression
|
||||
intensity weighting, character-slot/cast expression override resolution, and
|
||||
per-character expression picking plus action-aware character-expression
|
||||
sanitizing live in `row_expression.py`; `prompt_builder.py` keeps public
|
||||
delegate wrappers.
|
||||
- hardcore position/action-filter choices, selected-position normalization,
|
||||
config JSON builders/parsers, focus-policy toggles, subcategory allow-list
|
||||
policy, position-key detection, category filtering, and item-template/axis
|
||||
filtering live in `hardcore_position_config.py`.
|
||||
- hardcore configured-cast role graph generation lives in
|
||||
`hardcore_role_graphs.py`; row generation reaches it through
|
||||
`row_role_graph.py` after item/axis metadata is selected.
|
||||
- fallback role graph wording lives in `hardcore_role_fallback.py`, covering
|
||||
solo rows, women-only rows, men-only rows, mixed group fallbacks, and support
|
||||
partner sentences.
|
||||
- interaction-style role graph wording lives in `hardcore_role_interaction.py`,
|
||||
covering foreplay, manual stimulation, body worship, clothing transitions,
|
||||
dominant guidance, camera performance, aftercare, and group coordination.
|
||||
- outercourse-specific role graph wording has started moving into action-family
|
||||
modules; `hardcore_role_outercourse.py` owns boobjob, testicle-sucking,
|
||||
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
|
||||
direct POV viewer phrasing for kneeling, face-sitting, sixty-nine,
|
||||
edge-supported, side-lying, chair, standing, and reclining oral positions.
|
||||
- penetration-specific role graph wording lives in
|
||||
`hardcore_role_penetration.py`, covering the main vaginal penetration
|
||||
position families while Krea POV rewriting keeps first-person geometry stable.
|
||||
- anal/double-contact role graph wording lives in `hardcore_role_anal.py`,
|
||||
covering rear-entry anal variants and front/back double-contact source
|
||||
geometry.
|
||||
- climax role graph wording lives in `hardcore_role_climax.py`, covering
|
||||
ejaculation aftermath placement for face/body/ass, lap, open-thigh,
|
||||
side-lying, and front/back group layouts.
|
||||
- camera option schema, orbit/Qwen translation, config parsing, camera
|
||||
directive text, and camera caption text live in `camera_config.py`;
|
||||
camera-scene prose and contextual scene composition mutation for coworking,
|
||||
library, and semi-public profiles live in `scene_camera_adapters.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
|
||||
directives, source role-graph viewer replacement, and shared composition
|
||||
cleanup live in `pov_policy.py`; prompt builder and Krea POV routes delegate
|
||||
to it.
|
||||
- shared hardcore environment-anchor cleanup lives in
|
||||
`hardcore_text_cleanup.py` and normalizes malformed pool joins before metadata
|
||||
reaches formatter routes.
|
||||
- shared hardcore action metadata lives in `hardcore_action_metadata.py`; custom
|
||||
rows now emit `action_family`, `position_family`, `position_key`, and
|
||||
`position_keys` so formatter routing and debugging do less keyword guessing.
|
||||
Krea, SDXL, and training-caption routes consume these fields when present.
|
||||
- shared row route metadata readers live in `route_metadata.py`, covering
|
||||
normalized action family, position family/keys, and route-specific formatter
|
||||
hints for Krea, SDXL, and training-caption routes. Position keys are strict
|
||||
by default, while SDXL can opt into legacy unknown key tags for compatibility.
|
||||
- final row and pair text normalization lives in `row_normalization.py`,
|
||||
covering trigger prepending, extra-positive append, negative merge/dedupe,
|
||||
caption-part joining, embedded soft/hard row output synchronization, and row
|
||||
sanitation before metadata leaves generation. It also copies side-specific
|
||||
pair metadata, such as soft partner styling and hardcore clothing/detail
|
||||
state, plus shared cast descriptors, onto the embedded soft/hard rows.
|
||||
- final custom-row assembly now lives in `row_assembly.py` behind
|
||||
`CustomRowAssemblyRequest`, covering render context population,
|
||||
prompt/caption rendering delegation, row-base indexing, row metadata copying,
|
||||
configured-cast count metadata, profile/slot metadata, and
|
||||
disabled-expression cleanup.
|
||||
|
||||
### Pair / Adapter Layer
|
||||
|
||||
Owner today: `build_insta_of_pair`.
|
||||
Owner today: `pair_builder.py`; `prompt_builder.build_insta_of_pair` is the
|
||||
public wrapper used by the node layer.
|
||||
|
||||
Keep here:
|
||||
|
||||
- soft/hard row creation;
|
||||
- continuity policy;
|
||||
- softcore cast policy;
|
||||
- pair-level camera routing;
|
||||
- pair metadata shape.
|
||||
- the public wrapper signature and dependency bridge needed by existing nodes
|
||||
and tests.
|
||||
|
||||
Improve later:
|
||||
Already isolated:
|
||||
|
||||
- make a single pair metadata sanitizer that normalizes `softcore_row`,
|
||||
`hardcore_row`, pair prompts, negatives, captions, and camera fields;
|
||||
- split pair assembly into small functions by phase:
|
||||
`build_soft_row`, `build_hard_row`, `resolve_pair_camera`,
|
||||
`resolve_pair_clothing`, `assemble_pair_metadata`.
|
||||
- Insta/OF option normalization, softcore category/outfit/pose pools, partner
|
||||
outfit pools, clothing-continuity labels, negatives, and hardcore cast count
|
||||
policy, plus hardcore detail-density directive text, live in
|
||||
`pair_options.py`; `prompt_builder.py` keeps public delegate wrappers for
|
||||
existing nodes and tests.
|
||||
- pair route sequencing now lives in `pair_builder.py` behind
|
||||
`InstaPairBuildRequest` and `InstaPairBuildDependencies`, covering
|
||||
option/filter/seed/cast parsing handoff, soft/hard row orchestration, cast
|
||||
context, camera route, clothing route, and final output assembly delegation.
|
||||
- soft/hard row creation lives in `pair_rows.py` behind `InstaPairRowsRoute`,
|
||||
including softcore expression override resolution, Woman A slot context
|
||||
application, soft outfit/pose overrides, POV row fields, hardcore row
|
||||
creation, and legacy dict compatibility.
|
||||
- pair-level cast/display context lives in `pair_cast.py`, including descriptor
|
||||
prose, descriptor-entry assembly, shared descriptors, cast-label cleanup,
|
||||
same-cast softcore descriptor text, partner styling, platform and level
|
||||
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
|
||||
`InstaPairCameraRoute`, including soft/hard camera config selection,
|
||||
same-as-softcore mode, camera-detail override, same-room hard scene
|
||||
continuity, camera-aware composition mutation, POV camera suppression,
|
||||
row/root camera metadata synchronization, and legacy dict compatibility.
|
||||
- pair-level clothing policy lives in `pair_clothing.py` behind
|
||||
`HardcorePairClothingRoute`, including clothing sentence formatting,
|
||||
body-exposure scene cleanup, action-aware body-access flags, conflicting
|
||||
outfit-piece cleanup, default visible-men clothing, character-clothing
|
||||
override handling, hardcore clothing continuity, final root clothing-state
|
||||
assembly, and legacy dict compatibility.
|
||||
- final pair output assembly lives in `pair_output.py`, including soft/hard
|
||||
prompt strings, trigger preservation, negatives, captions, and root metadata
|
||||
shape; the final cleanup step is delegated to `row_normalization.py`.
|
||||
Embedded soft/hard rows are synchronized to the final pair prompt, caption,
|
||||
and negative outputs during normalization so serialized pair metadata does
|
||||
not carry stale standalone row text. Side-specific structured fields are
|
||||
synchronized there too, including soft partner styling, hardcore clothing
|
||||
continuity metadata, and shared cast descriptors for same-cast caption and
|
||||
formatter routes.
|
||||
|
||||
### Krea2 Formatter Path
|
||||
|
||||
@@ -116,20 +392,71 @@ Owner: `krea_formatter.py`.
|
||||
Keep here:
|
||||
|
||||
- Krea prose style;
|
||||
- cast prose;
|
||||
- hardcore action sentence rewriting;
|
||||
- POV sentence rewriting;
|
||||
- clothing naturalization;
|
||||
- Krea top-level route orchestration;
|
||||
- camera-scene preservation;
|
||||
- fallback text parsing.
|
||||
|
||||
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 prose assembly behind `KreaConfiguredCastRequest`,
|
||||
`KreaConfiguredCastDependencies`, and `KreaConfiguredCastPrompt`;
|
||||
`krea_formatter.py` keeps configured-cast detection and compatibility
|
||||
wrapper helpers.
|
||||
- `krea_normal_formatter.py` owns normal metadata single/couple/generic Krea
|
||||
prose assembly behind `KreaNormalRowRequest`, `KreaNormalRowDependencies`,
|
||||
and `KreaNormalRowPrompt`; `krea_formatter.py` keeps route selection.
|
||||
- `krea_row_fields.py` owns shared normal-row Krea field extraction for item,
|
||||
scene, pose, expression, composition/source-composition, camera, and style so
|
||||
normal and configured-cast Krea routes cannot drift independently.
|
||||
- `krea_pair_formatter.py` owns Insta/OF pair soft/hard Krea prose assembly
|
||||
behind `KreaPairFormatRequest`, `KreaPairFormatDependencies`, and
|
||||
`KreaPairPrompts`; `krea_formatter.py` keeps the `_insta_pair_to_krea`
|
||||
compatibility wrapper.
|
||||
- `krea_cast.py` owns cast descriptor parsing, cast labels, cast prose, label
|
||||
joining, natural cast descriptor text, and label replacement for formatter
|
||||
routes, including the caption naturalizer's cast metadata path.
|
||||
- `krea_clothing.py` owns clothing-state cleanup and action-aware body-access
|
||||
wording for formatter routes.
|
||||
- `krea_action_context.py` owns shared action-family predicates, axis context
|
||||
text, climax detection, and detail-density normalization used by action and
|
||||
POV formatter routes.
|
||||
- `hardcore_action_metadata.py` owns shared action-family constants,
|
||||
normalization, and inference used by the builder and Krea formatter route.
|
||||
- `pov_policy.py` owns shared POV labels, label filtering, source role-graph
|
||||
viewer replacement, and composition cleanup; `krea_pov.py` owns Krea-specific
|
||||
POV camera support text while delegating shared POV policy.
|
||||
- `krea_detail.py` owns generic detail-clause splitting, deduping, joining, and
|
||||
density limiting for Krea action prose.
|
||||
- `krea_action_positions.py` owns non-POV pose anchors, body-arrangement text,
|
||||
rear-entry detection, and action-position phrasing.
|
||||
- `krea_action_details.py` owns non-climax item/detail cleanup for foreplay,
|
||||
outercourse, oral, penetration, toy/double-contact, and anchor dedupe paths.
|
||||
- `krea_action_climax.py` owns climax-specific role/detail cleanup and aftermath
|
||||
view dedupe.
|
||||
- `krea_action_dispatch.py` owns non-POV role normalization, action-family
|
||||
classification, and family-specific detail cleanup.
|
||||
- `krea_actions.py` owns final non-POV hardcore action sentence assembly.
|
||||
- `krea_pov_actions.py` owns POV hardcore action sentence rewriting,
|
||||
first-person body geometry, and selected-position-axis priority before loose
|
||||
context fallback.
|
||||
- `formatter_input.py` owns shared metadata/source JSON detection, trigger
|
||||
stripping, the shared prompt field-label inventory, prompt-field extraction,
|
||||
`Avoid:` splitting, and row-value fallback for Krea, SDXL, and caption
|
||||
routes.
|
||||
- `route_metadata.py` owns shared row-level action-family, position-family,
|
||||
position-key, and formatter-hint reads so formatter routes do not normalize
|
||||
these fields independently.
|
||||
|
||||
Improve later:
|
||||
|
||||
- split semantic blocks into modules:
|
||||
`krea_cast.py`, `krea_actions.py`, `krea_pov.py`, `krea_clothing.py`;
|
||||
- add route-level smoke fixtures for representative metadata rows;
|
||||
- make `_hardcore_action_sentence` dispatch by action family instead of long
|
||||
conditional chains.
|
||||
- keep adding route-level smoke fixtures when new metadata fields start
|
||||
influencing formatter output;
|
||||
|
||||
### SDXL Formatter Path
|
||||
|
||||
@@ -139,16 +466,38 @@ Keep here:
|
||||
|
||||
- trigger behavior;
|
||||
- style and quality presets;
|
||||
- tag ordering;
|
||||
- weighted explicit tags;
|
||||
- final style/body/quality prompt assembly;
|
||||
- nude-weight setting;
|
||||
- negative-prompt assembly.
|
||||
|
||||
Improve later:
|
||||
Already isolated:
|
||||
|
||||
- move presets into data dictionaries or JSON so adding styles does not require
|
||||
editing formatter logic;
|
||||
- add formatter profiles for Pony, SDXL photo, and flat vector;
|
||||
- make fallback cleanup use the shared field-label inventory.
|
||||
- `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
|
||||
tag extraction behind `SDXLRowTagRequest`, `SDXLPairTagRequest`,
|
||||
`SDXLTagRouteDependencies`, and `SDXLTagRoute`; `sdxl_formatter.py` keeps
|
||||
compatibility wrappers plus final style/quality/trigger assembly.
|
||||
- `sdxl_tag_policy.py` owns SDXL tag splitting, tag-key dedupe, count inference,
|
||||
character descriptor tags, metadata-family hint tags, camera tags,
|
||||
explicit/nude helper tags, and route dependency assembly.
|
||||
- metadata-family tag hint data from `action_family`, `position_family`, and
|
||||
`position_keys` stays in `sdxl_presets.py` and is read by `sdxl_tag_policy.py`.
|
||||
- shared row route metadata reads from `route_metadata.py`.
|
||||
- shared formatter input parsing from `formatter_input.py`.
|
||||
- style presets, quality presets, default negative prompt, and action/position
|
||||
family tag hints from `sdxl_presets.py`.
|
||||
- formatter profiles for manual controls, Pony flat-vector, SDXL photo, and
|
||||
plain flat-vector styles live in `sdxl_presets.py` and are exposed by
|
||||
`SxCP SDXL Formatter`.
|
||||
- fallback field-label cleanup delegates to `formatter_input.py`.
|
||||
|
||||
Improve later:
|
||||
- add route-level fixtures for any new SDXL model profile that needs different
|
||||
tag ordering.
|
||||
|
||||
### Naturalizer Path
|
||||
|
||||
@@ -156,14 +505,40 @@ Owner: `caption_naturalizer.py`.
|
||||
|
||||
Keep here:
|
||||
|
||||
- natural sentence caption assembly;
|
||||
- top-level natural caption orchestration;
|
||||
- training-caption trigger behavior;
|
||||
- style-tail policy.
|
||||
- style-tail policy from `caption_policy.py`.
|
||||
|
||||
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
|
||||
single, couple, configured-cast, group/layout, and Insta/OF pair routes behind
|
||||
`CaptionMetadataRouteRequest`, `CaptionMetadataRouteDependencies`, and
|
||||
`CaptionMetadataRoute`; `caption_naturalizer.py` keeps compatibility wrappers,
|
||||
profile handling, trigger behavior, and text fallback.
|
||||
- `caption_text_policy.py` owns caption sentence helpers, trigger wrapping,
|
||||
formatter-hint append, row-value fallback wrappers, cast text wrappers,
|
||||
single-caption front parsing, route dependency assembly, and caption metadata
|
||||
helper callbacks used by `caption_metadata_routes.py`.
|
||||
- metadata-family action labels from `action_family` and `position_family` via
|
||||
`caption_policy.py`.
|
||||
- shared row route metadata reads from `route_metadata.py`.
|
||||
- shared formatter input parsing from `formatter_input.py`.
|
||||
- shared cast descriptor parsing and label replacement from `krea_cast.py`.
|
||||
- caption detail-level/style-policy normalization, clothing cleanup, and
|
||||
composition cleanup from `caption_policy.py`.
|
||||
- caption profiles for manual controls, concise training captions, dense
|
||||
training captions, and browsing captions live in `caption_policy.py` and are
|
||||
exposed by `SxCP Caption Naturalizer`.
|
||||
|
||||
Improve later:
|
||||
|
||||
- share more metadata readers with Krea without sharing Krea prose;
|
||||
- add a `caption_profile` option for concise/dense LoRA caption styles.
|
||||
- add more caption profiles if a new training or browsing workflow needs a
|
||||
distinct default.
|
||||
|
||||
### Category JSON Path
|
||||
|
||||
@@ -175,18 +550,27 @@ Keep here:
|
||||
- named scene/expression/composition pools;
|
||||
- item templates and axes;
|
||||
- direct category-specific wording.
|
||||
- optional object-style item templates with route metadata such as
|
||||
`action_family`, `action_type`, `position_family`, `family`, `position_key`,
|
||||
`position_keys`, and `formatter_hint`; string templates remain valid and fall
|
||||
back to Python inference. Normalized formatter hints are routed into Krea,
|
||||
SDXL, and caption naturalization through `all` plus the matching formatter
|
||||
route only.
|
||||
|
||||
Improve later:
|
||||
|
||||
- introduce optional `family` and `action_type` fields on item templates so
|
||||
Python filters do less keyword guessing;
|
||||
- add `formatter_hint` fields only where needed, not globally;
|
||||
- add a JSON audit that checks every referenced expression/composition/scene pool
|
||||
exists.
|
||||
- keep `tools/prompt_map_audit.py` passing; it now checks referenced
|
||||
expression/composition/scene pools, item-template axes, object-template
|
||||
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
|
||||
|
||||
Owner: `__init__.py`, `loop_nodes.py`, `web/*.js`.
|
||||
Owner: `__init__.py`, `node_builder.py`, `node_seed_resolution.py`,
|
||||
`node_camera.py`, `node_character.py`, `node_hardcore_position.py`,
|
||||
`node_formatter.py`, `node_insta.py`, `node_route_config.py`,
|
||||
`node_profile_filter.py`, `loop_nodes.py`, `web/*.js`.
|
||||
|
||||
Keep here:
|
||||
|
||||
@@ -194,13 +578,68 @@ Keep here:
|
||||
- widget behavior;
|
||||
- button actions;
|
||||
- dynamic input slots.
|
||||
- direct and config-driven builder node declarations in `node_builder.py`.
|
||||
- seed and resolution utility node declarations in `node_seed_resolution.py`.
|
||||
- camera utility node declarations in `node_camera.py`.
|
||||
- character pool, slot, and profile node declarations in `node_character.py`.
|
||||
- hardcore position pool/filter node declarations in
|
||||
`node_hardcore_position.py`.
|
||||
- caption/Krea2/SDXL formatter node declarations in `node_formatter.py`.
|
||||
- Insta/OF options and prompt-pair node declarations in `node_insta.py`.
|
||||
- route/category/location/composition/cast config node declarations in
|
||||
`node_route_config.py`.
|
||||
- profile/filter/ethnicity-list node declarations in `node_profile_filter.py`.
|
||||
|
||||
Already isolated:
|
||||
|
||||
- direct and config-driven prompt builder nodes live in `node_builder.py`, with
|
||||
registration maps imported by `__init__.py`.
|
||||
- seed axis salts/aliases, seed mode choices, lock builders, seed config
|
||||
parsing, row seed math, and deterministic axis RNG live in `seed_config.py`;
|
||||
seed/global-seed/seed-locker nodes live in `node_seed_resolution.py`, with
|
||||
registration maps imported by `__init__.py`.
|
||||
- SDXL/Krea2 resolution utility nodes live in `node_seed_resolution.py`, with
|
||||
registration maps imported by `__init__.py`.
|
||||
- camera/orbit/Qwen translator utility nodes live in `node_camera.py`, using
|
||||
`camera_config.py` for option lists and JSON builders, with registration maps
|
||||
imported by `__init__.py`.
|
||||
- hair, age/body/eyes/clothing pools, manual character details, character
|
||||
slots, and profile save/load nodes live in `node_character.py`, with
|
||||
registration maps imported by `__init__.py`.
|
||||
- hardcore position pool and action filter nodes live in
|
||||
`node_hardcore_position.py`, with registration maps imported by
|
||||
`__init__.py`.
|
||||
- caption naturalizer, Krea2 formatter, and SDXL formatter nodes live in
|
||||
`node_formatter.py`, with registration maps imported by `__init__.py`.
|
||||
- Insta/OF options and dual prompt-pair nodes live in `node_insta.py`, with
|
||||
registration maps imported by `__init__.py`.
|
||||
- category preset, location/composition pool, location theme, and cast config
|
||||
utility nodes live in `node_route_config.py`, with registration maps imported
|
||||
by `__init__.py`.
|
||||
- generation profile, advanced filter, and ethnicity list utility nodes live in
|
||||
`node_profile_filter.py`, with registration maps imported by `__init__.py`.
|
||||
- index-switch constants, index-base normalization, missing-input behavior,
|
||||
route-output selection, status text, and lazy-input selection live in
|
||||
`index_switch_policy.py`; `loop_nodes.py` keeps the ComfyUI node wrapper and
|
||||
accumulator/loop runtime logic.
|
||||
- node input tooltip inventory, node-specific tooltip overrides, dynamic input
|
||||
fallback tooltip rules, and tooltip injection live in `node_tooltips.py`;
|
||||
`__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
|
||||
`server_routes.py`; `__init__.py` only wires those pure handlers to ComfyUI
|
||||
JSON responses, and `tools/prompt_smoke.py` covers the handlers without
|
||||
importing ComfyUI.
|
||||
|
||||
Improve later:
|
||||
|
||||
- split large node classes into files by family;
|
||||
- split remaining large node classes into files by family;
|
||||
- keep node display names, return names, and docs in sync through the audit
|
||||
helper;
|
||||
- add small endpoint tests for profile/accumulator/index-switch routes.
|
||||
- add more endpoint tests when new server routes are introduced.
|
||||
|
||||
## Path-Specific Improvements
|
||||
|
||||
@@ -209,30 +648,38 @@ Improve later:
|
||||
Near-term:
|
||||
|
||||
- Add final row hygiene already done through `prompt_hygiene.py`.
|
||||
- Add a metadata smoke checker for representative rows through
|
||||
`tools/prompt_smoke.py`.
|
||||
- Add a metadata smoke checker for representative generated rows and static
|
||||
formatter fixtures through `tools/prompt_smoke.py`.
|
||||
- Normalize every row with one function before JSON serialization.
|
||||
|
||||
Medium-term:
|
||||
|
||||
- Extract category loading and role graph logic.
|
||||
- Convert keyword-heavy interaction filtering to template metadata.
|
||||
- Keep category loading, prompt-row routing, and role-graph routing in their
|
||||
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
|
||||
|
||||
Near-term:
|
||||
|
||||
- Normalize pair metadata with one helper.
|
||||
- Normalize pair metadata with one helper, including embedded row prompt,
|
||||
caption, negative, and side-specific metadata synchronization.
|
||||
- Confirm pair prompts, captions, and soft/hard rows carry the same sanitized
|
||||
scene/camera/clothing fields.
|
||||
- Keep same-room pair continuity synchronized in both assembled prompt text and
|
||||
`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:
|
||||
|
||||
- Make pair camera and clothing phases explicit subfunctions.
|
||||
- Add smoke fixtures for same-cast, POV man, explicit nude, and different-camera
|
||||
modes.
|
||||
- Keep camera and clothing phases in `pair_camera.py` and `pair_clothing.py`;
|
||||
extend those modules when a pair output shows concrete camera/clothing drift.
|
||||
- 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
|
||||
|
||||
@@ -241,13 +688,32 @@ Near-term:
|
||||
- Add final prose hygiene already done through `prompt_hygiene.py`.
|
||||
- Add smoke coverage through `tools/prompt_smoke.py` for metadata-driven Krea2
|
||||
formatting across built-in rows, hardcore rows, same-cast pairs, and POV
|
||||
pairs. Expand it next for close foreplay, POV penetration, and camera-scene
|
||||
preservation.
|
||||
pairs.
|
||||
- Cover camera-scene preservation through `tools/prompt_smoke.py` for single
|
||||
rows, split soft/hard pair cameras, and POV camera-scene routing.
|
||||
- Cover config-node routing through `tools/prompt_smoke.py` for category, cast,
|
||||
generation profile, seed lock, camera, location theme, and composition config.
|
||||
- Cover close foreplay and POV penetration Krea routes so raw labels, invalid
|
||||
surface grammar, normal third-person camera text, and composition punctuation
|
||||
drift are caught.
|
||||
- Cover POV outercourse, oral, penetration, anal, and front/back double-contact
|
||||
Krea routes so selected position geometry stays synchronized with metadata.
|
||||
- Cover generated climax routes through Krea, SDXL, and natural caption outputs
|
||||
so source aftermath placement and formatter details cannot drift apart.
|
||||
- Cover generated interaction routes through Krea, SDXL, and natural caption
|
||||
outputs so source contact/guidance/presentation wording stays metadata-driven.
|
||||
- Cover generated fallback role routes through Krea, SDXL, and natural caption
|
||||
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:
|
||||
|
||||
- Dispatch action rewriting by action family.
|
||||
- Split Krea semantic helpers into smaller modules.
|
||||
- Keep action-family rewriting dispatched through `krea_action_dispatch.py` and
|
||||
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
|
||||
|
||||
@@ -281,10 +747,13 @@ Near-term:
|
||||
- Keep scene-camera adapters scoped by location family.
|
||||
- Use the memory note in
|
||||
`/home/ethanfel/.codex/memories/scene-camera-system.md` when editing POV.
|
||||
- Keep `scene_camera_adapters.py` as the owner for location-aware camera prose;
|
||||
add new location families there one at a time.
|
||||
- Keep `row_camera.py` as the owner for inserting camera/scene directives into
|
||||
generated rows, including POV suppression of normal third-person camera text.
|
||||
|
||||
Medium-term:
|
||||
|
||||
- Move coworking adapter into a scene-camera adapter module.
|
||||
- Build new adapters one location family at a time.
|
||||
|
||||
## Invariants To Preserve
|
||||
@@ -300,10 +769,10 @@ Medium-term:
|
||||
|
||||
## Recommended Next Passes
|
||||
|
||||
1. Expand `tools/prompt_smoke.py` with camera-scene, explicit nude, and
|
||||
different-camera pair fixtures.
|
||||
2. Split Krea action/POV/clothing helpers into separate modules.
|
||||
3. Add category JSON pool reference validation to `tools/prompt_map_audit.py`.
|
||||
4. Extract scene-camera adapters from `prompt_builder.py`.
|
||||
5. Split `__init__.py` node classes by family after behavior is covered by smoke
|
||||
checks.
|
||||
1. Keep new node classes in their owning `node_*.py` or `loop_nodes.py`
|
||||
module, with registration/display docs covered by the audit.
|
||||
2. Keep `hardcore_role_graphs.py` as the dispatch surface; add behavior in the
|
||||
existing `hardcore_role_*` action-family modules only when a concrete
|
||||
generated edge case needs it.
|
||||
3. Add route-level smoke fixtures only for observed generated edge cases or new
|
||||
metadata fields that affect Krea2, SDXL, or caption output.
|
||||
|
||||
+562
-151
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,854 @@
|
||||
# Clothing Seed Axis Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a first-class `clothing` seed axis so workflows can keep content, pose, role, person, scene, expression, and composition identical while rerolling only clothing/outfit choices.
|
||||
|
||||
**Architecture:** Extend the shared seed policy first, then route clothing selections through `axis_rng(..., "clothing", ...)` in prompt and pair flows. Keep `content` responsible for content item/template choices, use `outfit_seed` as a clothing alias, and keep `content_seed` as a compatibility fallback only when no explicit clothing/outfit seed exists.
|
||||
|
||||
**Tech Stack:** Python 3, ComfyUI custom nodes, local smoke tests in `tools/prompt_smoke.py`, scene nodes in `node_scene.py`, seed policy in `seed_config.py`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify `seed_config.py`: add the `clothing` seed axis, aliases, reroll groups, lock config emission, and trace support through the existing generic functions.
|
||||
- Modify `prompt_builder.py`: expose optional `clothing_seed` and `clothing_seed_mode` through the wrapper around `seed_config.build_seed_config_json`.
|
||||
- Modify `node_seed_resolution.py`: expose clothing controls in `SxCPSeedControl` and let `SxCPSeedLocker` pick up the new reroll choices from `seed_config`.
|
||||
- Modify `node_tooltips.py`: add help text for the new manual clothing seed controls and update the seed-locker tooltip.
|
||||
- Modify `builder_prompt_route.py`: use a clothing RNG for prompt clothing mode selection.
|
||||
- Modify `pair_rows.py`: use a clothing RNG for primary softcore outfit selection in scene pairs.
|
||||
- Modify `pair_cast.py`: use a clothing RNG for secondary pair participant outfits.
|
||||
- Modify `node_scene.py`: map scene layer seed axes to `clothing`, `content_clothing`, and `clothing_pose`.
|
||||
- Modify `tools/prompt_smoke.py`: add red/green smoke coverage for seed vocabulary, UI inputs, routing, and scene-pair clothes-only rerolls.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Shared Seed Vocabulary And UI Surface
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/prompt_smoke.py`
|
||||
- Modify: `seed_config.py`
|
||||
- Modify: `prompt_builder.py`
|
||||
- Modify: `node_seed_resolution.py`
|
||||
- Modify: `node_tooltips.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing seed vocabulary smoke tests**
|
||||
|
||||
In `tools/prompt_smoke.py`, inside `smoke_seed_config_policy()`, after the existing `normalize_reroll_axis("content pose")` assertion, add:
|
||||
|
||||
```python
|
||||
reroll_choices = pb.seed_reroll_axis_choices()
|
||||
for expected_axis in ("clothing", "content_clothing", "clothing_pose"):
|
||||
_expect(expected_axis in reroll_choices, f"seed reroll axis choices missing {expected_axis}")
|
||||
_expect(pb.normalize_reroll_axis("clothing pose") == "clothing_pose", "reroll axis normalizer should accept clothing pose")
|
||||
_expect(pb.normalize_reroll_axis("content clothing") == "content_clothing", "reroll axis normalizer should accept content clothing")
|
||||
```
|
||||
|
||||
In the same function, replace:
|
||||
|
||||
```python
|
||||
parsed = pb._parse_seed_config({"item_seed": "44", "pose_seed": "55", "bad": "nope"})
|
||||
_expect(parsed == {"item_seed": 44, "pose_seed": 55}, "seed parser should keep integer-like values only")
|
||||
_expect(pb._configured_axis_seed(parsed, "content") == 44, "content axis should honor item_seed alias")
|
||||
_expect(pb._configured_axis_seed(parsed, "role") == 55, "role axis should honor pose seed alias")
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
parsed = pb._parse_seed_config({"item_seed": "44", "pose_seed": "55", "outfit_seed": "66", "bad": "nope"})
|
||||
_expect(
|
||||
parsed == {"item_seed": 44, "pose_seed": 55, "outfit_seed": 66},
|
||||
"seed parser should keep integer-like values only",
|
||||
)
|
||||
_expect(pb._configured_axis_seed(parsed, "content") == 44, "content axis should honor item_seed alias")
|
||||
_expect(pb._configured_axis_seed(parsed, "clothing") == 66, "clothing axis should honor outfit_seed alias")
|
||||
_expect(
|
||||
pb._configured_axis_seed({"content_seed": 77}, "clothing") == 77,
|
||||
"clothing axis should keep content_seed as a legacy fallback",
|
||||
)
|
||||
_expect(
|
||||
pb._configured_axis_seed({"content_seed": 77, "clothing_seed": 88}, "clothing") == 88,
|
||||
"clothing_seed should override legacy content_seed fallback",
|
||||
)
|
||||
_expect(pb._configured_axis_seed(parsed, "role") == 55, "role axis should honor pose seed alias")
|
||||
```
|
||||
|
||||
In the same function, after the existing `locked = json.loads(...)` block and its three assertions, add:
|
||||
|
||||
```python
|
||||
clothing_locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="clothing", reroll_seed=777))
|
||||
_expect(clothing_locked["clothing_seed"] == 777, "clothing reroll should alter clothing seed")
|
||||
_expect(clothing_locked["content_seed"] == 100, "clothing reroll should leave content locked")
|
||||
_expect(clothing_locked["pose_seed"] == 100 and clothing_locked["role_seed"] == 100, "clothing reroll should leave pose and role locked")
|
||||
|
||||
content_clothing_locked = json.loads(
|
||||
pb.build_seed_lock_config_json(base_seed=100, reroll_axis="content_clothing", reroll_seed=778)
|
||||
)
|
||||
_expect(content_clothing_locked["content_seed"] == 778, "content_clothing reroll should alter content seed")
|
||||
_expect(content_clothing_locked["clothing_seed"] == 778, "content_clothing reroll should alter clothing seed")
|
||||
_expect(content_clothing_locked["pose_seed"] == 100, "content_clothing reroll should leave pose locked")
|
||||
|
||||
clothing_pose_locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="clothing_pose", reroll_seed=779))
|
||||
_expect(clothing_pose_locked["clothing_seed"] == 779, "clothing_pose reroll should alter clothing seed")
|
||||
_expect(clothing_pose_locked["pose_seed"] == 779 and clothing_pose_locked["role_seed"] == 779, "clothing_pose reroll should alter pose and role seeds")
|
||||
_expect(clothing_pose_locked["content_seed"] == 100, "clothing_pose reroll should leave content locked")
|
||||
|
||||
content_pose_locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="content_pose", reroll_seed=780))
|
||||
_expect(content_pose_locked["clothing_seed"] == 100, "content_pose reroll should not alter clothing seed")
|
||||
```
|
||||
|
||||
Change the `axis_trace` test block from:
|
||||
|
||||
```python
|
||||
axis_trace = seed_config.axis_seed_trace({"content_seed": 44}, 99, 3, axes=("content", "scene"))
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
axis_trace = seed_config.axis_seed_trace({"content_seed": 44, "clothing_seed": 66}, 99, 3, axes=("content", "clothing", "scene"))
|
||||
```
|
||||
|
||||
Then add this assertion after the existing content seed assertions:
|
||||
|
||||
```python
|
||||
_expect(axis_trace["clothing"]["source"] == "configured", "Seed axis trace lost clothing configured source")
|
||||
_expect(axis_trace["clothing"]["seed"] == 66, "Seed axis trace lost configured clothing seed")
|
||||
```
|
||||
|
||||
In `tools/prompt_smoke.py`, inside `smoke_node_utility_registration()`, after:
|
||||
|
||||
```python
|
||||
_expect("category_seed_mode" in seed_inputs, "Seed Control lost category seed mode input")
|
||||
```
|
||||
|
||||
add:
|
||||
|
||||
```python
|
||||
_expect("clothing_seed_mode" in seed_inputs, "Seed Control lost clothing seed mode input")
|
||||
_expect("clothing_seed" in seed_inputs, "Seed Control lost clothing seed input")
|
||||
```
|
||||
|
||||
After the `category_seed_tooltip` assertion, add:
|
||||
|
||||
```python
|
||||
clothing_seed_tooltip = node_tooltips._tooltip_for_input("SxCPSeedControl", "clothing_seed_mode")
|
||||
_expect("clothing/outfit" in clothing_seed_tooltip, "Node tooltip policy lost Seed Control clothing override")
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```python
|
||||
_expect(int(parsed_seed_control.get("content_seed", -1)) >= 0, "Seed Control random mode did not emit resolved seed")
|
||||
```
|
||||
|
||||
add this assertion after updating the call in Step 3:
|
||||
|
||||
```python
|
||||
_expect(parsed_seed_control.get("clothing_seed") == 222, "Seed Control fixed clothing seed changed")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused smoke tests and verify they fail for the new behavior**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
```
|
||||
|
||||
Expected: FAIL with `seed reroll axis choices missing clothing`.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_utility_registration --quiet
|
||||
```
|
||||
|
||||
Expected: FAIL with `Seed Control lost clothing seed mode input`.
|
||||
|
||||
- [ ] **Step 3: Implement the shared seed axis**
|
||||
|
||||
In `seed_config.py`, update the top-level seed definitions:
|
||||
|
||||
```python
|
||||
SEED_AXIS_SALTS = {
|
||||
"category": 31,
|
||||
"subcategory": 37,
|
||||
"content": 41,
|
||||
"clothing": 41,
|
||||
"person": 43,
|
||||
"scene": 47,
|
||||
"pose": 53,
|
||||
"role": 57,
|
||||
"expression": 59,
|
||||
"composition": 61,
|
||||
}
|
||||
|
||||
SEED_AXIS_ALIASES = {
|
||||
"category": ("category_seed", "category"),
|
||||
"subcategory": ("subcategory_seed", "subcategory"),
|
||||
"content": ("content_seed", "item_seed", "sexual_pose_seed", "content"),
|
||||
"clothing": ("clothing_seed", "outfit_seed", "wardrobe_seed", "content_seed", "content"),
|
||||
"person": ("person_seed", "appearance_seed", "cast_seed", "person"),
|
||||
"scene": ("scene_seed", "scene"),
|
||||
"pose": ("pose_seed", "sexual_pose_seed", "pose"),
|
||||
"role": ("role_seed", "role", "pose_seed", "sexual_pose_seed"),
|
||||
"expression": ("expression_seed", "face_seed", "expression"),
|
||||
"composition": ("composition_seed", "camera_seed", "composition"),
|
||||
}
|
||||
|
||||
SEED_LOCK_AXES = (
|
||||
"category",
|
||||
"subcategory",
|
||||
"content",
|
||||
"clothing",
|
||||
"person",
|
||||
"scene",
|
||||
"pose",
|
||||
"role",
|
||||
"expression",
|
||||
"composition",
|
||||
)
|
||||
```
|
||||
|
||||
In the same file, update `SEED_REROLL_GROUPS`:
|
||||
|
||||
```python
|
||||
SEED_REROLL_GROUPS = {
|
||||
"none": (),
|
||||
"category": ("category",),
|
||||
"subcategory": ("subcategory",),
|
||||
"content": ("content",),
|
||||
"clothing": ("clothing",),
|
||||
"person": ("person",),
|
||||
"scene": ("scene",),
|
||||
"pose": ("pose", "role"),
|
||||
"role": ("role",),
|
||||
"expression": ("expression",),
|
||||
"composition": ("composition",),
|
||||
"content_pose": ("content", "pose", "role"),
|
||||
"content_clothing": ("content", "clothing"),
|
||||
"clothing_pose": ("clothing", "pose", "role"),
|
||||
"scene_pose": ("scene", "pose", "role"),
|
||||
}
|
||||
```
|
||||
|
||||
Update `normalize_reroll_axis()` aliases:
|
||||
|
||||
```python
|
||||
aliases = {
|
||||
"contentpose": "content_pose",
|
||||
"contentclothing": "content_clothing",
|
||||
"clothingpose": "clothing_pose",
|
||||
"scenepose": "scene_pose",
|
||||
}
|
||||
```
|
||||
|
||||
Update `build_seed_config_json()` in `seed_config.py` by adding parameters between `content_seed` and `person_seed`:
|
||||
|
||||
```python
|
||||
clothing_seed: int = -1,
|
||||
```
|
||||
|
||||
and between `content_seed_mode` and `person_seed_mode`:
|
||||
|
||||
```python
|
||||
clothing_seed_mode: str = "auto",
|
||||
```
|
||||
|
||||
Then add the emitted field after `content_seed`:
|
||||
|
||||
```python
|
||||
"clothing_seed": axis_seed(clothing_seed, clothing_seed_mode),
|
||||
```
|
||||
|
||||
In `prompt_builder.py`, update `build_seed_config_json()` with the same optional `clothing_seed` and `clothing_seed_mode` parameters, and pass them to `seed_policy.build_seed_config_json(...)`:
|
||||
|
||||
```python
|
||||
clothing_seed=clothing_seed,
|
||||
clothing_seed_mode=clothing_seed_mode,
|
||||
```
|
||||
|
||||
In `node_seed_resolution.py`, update `SxCPSeedControl.SEED_AXES`:
|
||||
|
||||
```python
|
||||
SEED_AXES = (
|
||||
"category",
|
||||
"subcategory",
|
||||
"content",
|
||||
"clothing",
|
||||
"person",
|
||||
"scene",
|
||||
"pose",
|
||||
"role",
|
||||
"expression",
|
||||
"composition",
|
||||
)
|
||||
```
|
||||
|
||||
Update `SxCPSeedControl.build(...)` by adding `clothing_seed_mode, clothing_seed` after `content_seed`, and pass both into `build_seed_config_json(...)`:
|
||||
|
||||
```python
|
||||
clothing_seed=clothing_seed,
|
||||
clothing_seed_mode=clothing_seed_mode,
|
||||
```
|
||||
|
||||
In `tools/prompt_smoke.py`, update the `seed_control().build(...)` call in `smoke_node_utility_registration()` by inserting these two arguments after the current content seed pair:
|
||||
|
||||
```python
|
||||
"fixed",
|
||||
222,
|
||||
```
|
||||
|
||||
In `node_tooltips.py`, update `NODE_INPUT_TOOLTIPS["SxCPSeedControl"]` with:
|
||||
|
||||
```python
|
||||
"clothing_seed_mode": "Controls clothing/outfit selection separately from content item selection.",
|
||||
"clothing_seed": "Seed used when clothing_seed_mode is fixed or auto with a non-negative value.",
|
||||
```
|
||||
|
||||
Update `NODE_INPUT_TOOLTIPS["SxCPSeedLocker"]["reroll_axis"]` to mention clothing:
|
||||
|
||||
```python
|
||||
"reroll_axis": "Choose the one axis to change while the rest stays locked. Use clothing for outfit-only rerolls, pose for sexual pose, scene for location, person for appearance.",
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the focused smoke tests and verify they pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_utility_registration --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
- [ ] **Step 5: Commit Task 1**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git --git-dir=.git-real --work-tree=. add seed_config.py prompt_builder.py node_seed_resolution.py node_tooltips.py tools/prompt_smoke.py
|
||||
git --git-dir=.git-real --work-tree=. commit -m "Add clothing seed axis vocabulary"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Route Prompt And Pair Clothing Through The Clothing Axis
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/prompt_smoke.py`
|
||||
- Modify: `builder_prompt_route.py`
|
||||
- Modify: `pair_rows.py`
|
||||
- Modify: `pair_cast.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing prompt-routing smoke test**
|
||||
|
||||
In `tools/prompt_smoke.py`, inside `smoke_seed_config_policy()`, after the existing `pose_changed` assertion block, add:
|
||||
|
||||
```python
|
||||
clothes_base_seed = 52001
|
||||
clothes_base_config = json.loads(pb.build_seed_lock_config_json(base_seed=clothes_base_seed))
|
||||
|
||||
def clothes_row(clothing_seed: int) -> dict[str, Any]:
|
||||
seed_config_for_row = dict(clothes_base_config)
|
||||
seed_config_for_row["clothing_seed"] = clothing_seed
|
||||
return _prompt_row(
|
||||
name=f"seed_config_policy_clothing_seed_{clothing_seed}",
|
||||
category="woman",
|
||||
subcategory="",
|
||||
seed=clothes_base_seed,
|
||||
seed_config=seed_config_for_row,
|
||||
clothing="random",
|
||||
minimal_clothing_ratio=0.5,
|
||||
character_cast=_character_cast(),
|
||||
location_config=_coworking_location_config(),
|
||||
)
|
||||
|
||||
clothes_locked_a = clothes_row(53001)
|
||||
clothes_changed = False
|
||||
for clothing_seed in range(53002, 53100):
|
||||
clothes_candidate = clothes_row(clothing_seed)
|
||||
_expect(
|
||||
clothes_candidate.get("scene_text") == clothes_locked_a.get("scene_text"),
|
||||
"clothing reroll should keep scene text stable",
|
||||
)
|
||||
_expect(
|
||||
clothes_candidate.get("pose") == clothes_locked_a.get("pose"),
|
||||
"clothing reroll should keep pose stable",
|
||||
)
|
||||
_expect(
|
||||
clothes_candidate.get("cast_descriptor_text") == clothes_locked_a.get("cast_descriptor_text"),
|
||||
"clothing reroll should keep cast descriptors stable",
|
||||
)
|
||||
if clothes_candidate.get("clothing") != clothes_locked_a.get("clothing"):
|
||||
clothes_changed = True
|
||||
break
|
||||
_expect(clothes_changed, "clothing_seed reroll should change prompt clothing mode")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused smoke test and verify it fails for the current routing**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
```
|
||||
|
||||
Expected: FAIL with `clothing_seed reroll should change prompt clothing mode`.
|
||||
|
||||
- [ ] **Step 3: Implement clothing RNG routing in normal prompt rows**
|
||||
|
||||
In `builder_prompt_route.py`, inside `build_prompt_result(...)`, replace:
|
||||
|
||||
```python
|
||||
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)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
content_rng = deps.axis_rng(parsed_seed_config, "content", seed, row_number)
|
||||
clothing_rng = deps.axis_rng(parsed_seed_config, "clothing", seed, row_number)
|
||||
pose_axis_rng = deps.axis_rng(parsed_seed_config, "pose", seed, row_number)
|
||||
```
|
||||
|
||||
Then replace:
|
||||
|
||||
```python
|
||||
clothing = deps.pick_clothing_mode(content_rng, clothing, minimal_ratio)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
clothing = deps.pick_clothing_mode(clothing_rng, clothing, minimal_ratio)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the focused smoke test and verify it passes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
- [ ] **Step 5: Write the failing pair-routing smoke tests**
|
||||
|
||||
In `tools/prompt_smoke.py`, inside `smoke_node_scene_chain_registration()`, find:
|
||||
|
||||
```python
|
||||
soft_content_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
|
||||
"softcore_branch",
|
||||
"fixed",
|
||||
6679,
|
||||
"content",
|
||||
"same_for_all_rows",
|
||||
"replace_layer",
|
||||
)[0]
|
||||
```
|
||||
|
||||
Replace it with:
|
||||
|
||||
```python
|
||||
soft_clothing_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
|
||||
"softcore_branch",
|
||||
"fixed",
|
||||
6679,
|
||||
"clothing",
|
||||
"same_for_all_rows",
|
||||
"replace_layer",
|
||||
)[0]
|
||||
```
|
||||
|
||||
In the same block, replace `seed_options=soft_content_seed_options` with:
|
||||
|
||||
```python
|
||||
seed_options=soft_clothing_seed_options,
|
||||
```
|
||||
|
||||
Update the expected failure message from:
|
||||
|
||||
```python
|
||||
"Scene softcore branch content seed fixture no longer selects the expected outfit",
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
"Scene softcore branch clothing seed fixture no longer selects the expected outfit",
|
||||
```
|
||||
|
||||
After the existing `soft_pose_pair` assertions and before the choice-board block, add:
|
||||
|
||||
```python
|
||||
def _soft_clothing_pair(soft_clothing_seed: int) -> dict[str, Any]:
|
||||
soft_clothing_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
|
||||
"softcore_branch",
|
||||
"fixed",
|
||||
soft_clothing_seed,
|
||||
"clothing",
|
||||
"same_for_all_rows",
|
||||
"replace_layer",
|
||||
)[0]
|
||||
soft_scene_clothing, hard_scene_clothing, _summary, _metadata = nodes["SxCPSceneBranchPair"]().build(
|
||||
scene,
|
||||
"same_creator_same_room",
|
||||
"hybrid",
|
||||
branch_options=branch_options,
|
||||
seed_options=soft_clothing_seed_options,
|
||||
)
|
||||
soft_scene_clothing = nodes["SxCPSoftcoreBranchOptions"]().build(
|
||||
soft_scene_clothing,
|
||||
"same_as_hardcore",
|
||||
"lingerie_tease",
|
||||
True,
|
||||
0.45,
|
||||
"from_camera_config",
|
||||
"compact",
|
||||
"",
|
||||
branch_options=branch_options,
|
||||
seed_options=soft_clothing_seed_options,
|
||||
)[0]
|
||||
hard_scene_clothing = nodes["SxCPHardcoreBranchOptions"]().build(
|
||||
hard_scene_clothing,
|
||||
"couple",
|
||||
1,
|
||||
1,
|
||||
"hardcore",
|
||||
True,
|
||||
0.85,
|
||||
"partially_removed",
|
||||
"from_camera_config",
|
||||
"compact",
|
||||
"balanced",
|
||||
"",
|
||||
branch_options=branch_options,
|
||||
)[0]
|
||||
return json.loads(nodes["SxCPScenePairOutput"]().build(soft_scene_clothing, hard_scene_clothing)[7])
|
||||
|
||||
soft_clothing_pairs = [_soft_clothing_pair(seed) for seed in (6677, 6678, 6679, 6680)]
|
||||
soft_clothing_items = {pair.get("softcore_row", {}).get("item") for pair in soft_clothing_pairs}
|
||||
soft_clothing_poses = {pair.get("softcore_row", {}).get("pose") for pair in soft_clothing_pairs}
|
||||
soft_clothing_hard_states = {pair.get("hardcore_clothing_state") for pair in soft_clothing_pairs}
|
||||
_expect(len(soft_clothing_items) > 1, "Softcore branch clothing reroll should change softcore outfit")
|
||||
_expect(len(soft_clothing_hard_states) > 1, "Softcore branch clothing reroll should change inherited hard clothing")
|
||||
_expect(len(soft_clothing_poses) == 1, "Softcore branch clothing reroll should keep softcore pose stable")
|
||||
for expected_seed, clothing_pair in zip((6677, 6678, 6679, 6680), soft_clothing_pairs):
|
||||
soft_seed_config = clothing_pair.get("softcore_row", {}).get("seed_config") if isinstance(clothing_pair.get("softcore_row"), dict) else {}
|
||||
hard_seed_config = clothing_pair.get("hardcore_row", {}).get("seed_config") if isinstance(clothing_pair.get("hardcore_row"), dict) else {}
|
||||
_expect(
|
||||
soft_seed_config.get("clothing_seed") == expected_seed,
|
||||
"Softcore branch clothing seed did not reach softcore generator seed config",
|
||||
)
|
||||
_expect(
|
||||
soft_seed_config.get("content_seed") != expected_seed,
|
||||
"Softcore branch clothing seed should not overwrite content seed",
|
||||
)
|
||||
_expect(
|
||||
hard_seed_config.get("clothing_seed") != expected_seed,
|
||||
"Softcore branch clothing seed leaked into hardcore generator seed config",
|
||||
)
|
||||
```
|
||||
|
||||
In the `content_pair` assertions, add:
|
||||
|
||||
```python
|
||||
_expect(
|
||||
content_hard_seed_config.get("clothing_seed") != 8899,
|
||||
"Hardcore branch content_pose reroll should not reach hardcore clothing seed",
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run the scene-chain smoke test and verify it fails for the missing scene axis/routing**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
|
||||
```
|
||||
|
||||
Expected: FAIL with either `Scene softcore branch clothing seed fixture no longer selects the expected outfit` or `Softcore branch clothing reroll should change softcore outfit`.
|
||||
|
||||
- [ ] **Step 7: Implement clothing RNG routing in pair rows**
|
||||
|
||||
In `pair_rows.py`, inside `build_insta_pair_rows_result(...)`, replace:
|
||||
|
||||
```python
|
||||
soft_content_rng = axis_rng(soft_seed_config, "content", seed, row_number + 311)
|
||||
soft_pose_rng = axis_rng(soft_seed_config, "pose", seed, row_number + 313)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
soft_content_rng = axis_rng(soft_seed_config, "content", seed, row_number + 311)
|
||||
soft_clothing_rng = axis_rng(soft_seed_config, "clothing", seed, row_number + 311)
|
||||
soft_pose_rng = axis_rng(soft_seed_config, "pose", seed, row_number + 313)
|
||||
```
|
||||
|
||||
Then replace:
|
||||
|
||||
```python
|
||||
primary_softcore_outfit = slot_softcore_outfit(primary_slot, soft_content_rng)
|
||||
soft_row["item"] = primary_softcore_outfit or softcore_outfit(soft_content_rng, softcore_level_key)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
primary_softcore_outfit = slot_softcore_outfit(primary_slot, soft_clothing_rng)
|
||||
soft_row["item"] = primary_softcore_outfit or softcore_outfit(soft_clothing_rng, softcore_level_key)
|
||||
```
|
||||
|
||||
In `pair_cast.py`, inside `softcore_partner_styling(...)`, replace:
|
||||
|
||||
```python
|
||||
content_rng = axis_rng(seed_config, "content", seed, row_number + 421)
|
||||
pose_rng = axis_rng(seed_config, "pose", seed, row_number + 421)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
content_rng = axis_rng(seed_config, "content", seed, row_number + 421)
|
||||
clothing_rng = axis_rng(seed_config, "clothing", seed, row_number + 421)
|
||||
pose_rng = axis_rng(seed_config, "pose", seed, row_number + 421)
|
||||
```
|
||||
|
||||
Then replace both `slot_softcore_outfit(..., content_rng)` calls with `slot_softcore_outfit(..., clothing_rng)`, and replace both outfit `choose(content_rng, ...)` calls with `choose(clothing_rng, ...)`.
|
||||
|
||||
- [ ] **Step 8: Run focused smoke tests and verify Task 2 behavior passes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
|
||||
```
|
||||
|
||||
Expected at this point: FAIL only on missing scene layer mapping, with a message involving the softcore branch clothing seed.
|
||||
|
||||
- [ ] **Step 9: Commit Task 2**
|
||||
|
||||
If `seed_config_policy` passes and `node_scene_chain_registration` now fails only because scene layer axes do not apply `clothing`, commit the prompt and pair routing work:
|
||||
|
||||
```bash
|
||||
git --git-dir=.git-real --work-tree=. add builder_prompt_route.py pair_rows.py pair_cast.py tools/prompt_smoke.py
|
||||
git --git-dir=.git-real --work-tree=. commit -m "Route clothing choices through clothing seed"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Scene Layer Clothing Axis Mapping
|
||||
|
||||
**Files:**
|
||||
- Modify: `node_scene.py`
|
||||
- Modify: `tools/prompt_smoke.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing scene-axis smoke assertions**
|
||||
|
||||
In `tools/prompt_smoke.py`, inside `smoke_node_scene_chain_registration()`, replace:
|
||||
|
||||
```python
|
||||
wardrobe_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build("wardrobe", "fixed", 9981, "content", "same_for_all_rows", "replace_layer")[0]
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
wardrobe_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build("wardrobe", "fixed", 9981, "clothing", "same_for_all_rows", "replace_layer")[0]
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```python
|
||||
_expect(json.loads(scene).get("seed_trace", {}).get("wardrobe", {}).get("seed") == 9981, "Scene Wardrobe seed options did not write seed trace")
|
||||
```
|
||||
|
||||
add:
|
||||
|
||||
```python
|
||||
_expect(
|
||||
json.loads(scene).get("seed_trace", {}).get("wardrobe", {}).get("axes") == ["clothing"],
|
||||
"Scene Wardrobe seed options should target clothing axis",
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run scene-chain smoke and verify it fails for missing scene layer clothing mapping**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
|
||||
```
|
||||
|
||||
Expected: FAIL with a message involving wardrobe or softcore branch clothing axis mapping.
|
||||
|
||||
- [ ] **Step 3: Implement scene layer clothing mappings**
|
||||
|
||||
In `node_scene.py`, update `SCENE_LAYER_SEED_AXES`:
|
||||
|
||||
```python
|
||||
SCENE_LAYER_SEED_AXES = {
|
||||
"cast": ("category",),
|
||||
"character": ("person",),
|
||||
"wardrobe": ("clothing",),
|
||||
"location": ("scene",),
|
||||
"set_dressing": ("scene",),
|
||||
"blocking": ("pose",),
|
||||
"action": ("pose", "role"),
|
||||
"performance": ("expression",),
|
||||
"camera": ("composition",),
|
||||
"composition": ("composition",),
|
||||
"lighting": ("composition",),
|
||||
"softcore_branch": ("clothing", "pose", "role"),
|
||||
"hardcore_branch": ("pose", "role"),
|
||||
}
|
||||
```
|
||||
|
||||
In the same file, update `SCENE_REROLL_GROUPS`:
|
||||
|
||||
```python
|
||||
SCENE_REROLL_GROUPS = {
|
||||
"none": (),
|
||||
"category": ("category",),
|
||||
"subcategory": ("subcategory",),
|
||||
"content": ("content",),
|
||||
"clothing": ("clothing",),
|
||||
"person": ("person",),
|
||||
"scene": ("scene",),
|
||||
"pose": ("pose", "role"),
|
||||
"role": ("role",),
|
||||
"expression": ("expression",),
|
||||
"composition": ("composition",),
|
||||
"content_pose": ("content", "pose", "role"),
|
||||
"content_clothing": ("content", "clothing"),
|
||||
"clothing_pose": ("clothing", "pose", "role"),
|
||||
"scene_pose": ("scene", "pose", "role"),
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run focused smoke tests and verify they pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_utility_registration --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
- [ ] **Step 5: Commit Task 3**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git --git-dir=.git-real --work-tree=. add node_scene.py tools/prompt_smoke.py
|
||||
git --git-dir=.git-real --work-tree=. commit -m "Map scene clothing seeds to clothing axis"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Final Verification And Push
|
||||
|
||||
**Files:**
|
||||
- No production file edits expected.
|
||||
- Verify all files touched by Tasks 1-3.
|
||||
|
||||
- [ ] **Step 1: Run compilation checks**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python -m py_compile seed_config.py prompt_builder.py node_seed_resolution.py node_tooltips.py builder_prompt_route.py pair_rows.py pair_cast.py node_scene.py tools/prompt_smoke.py
|
||||
```
|
||||
|
||||
Expected: exit code 0.
|
||||
|
||||
- [ ] **Step 2: Run focused smoke tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
python tools/prompt_smoke.py --case node_utility_registration --quiet
|
||||
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
|
||||
```
|
||||
|
||||
Expected for each command: `OK: smoke passed (1 cases).`
|
||||
|
||||
- [ ] **Step 3: Run the full smoke suite**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --quiet
|
||||
```
|
||||
|
||||
Expected: all cases pass except the known unrelated `krea2_prompt_guide_policy` failure if it is still present. If any new failure mentions seed policy, scene layer seed, clothing state, prompt routing, pair rows, or node utility registration, fix it before committing or pushing.
|
||||
|
||||
- [ ] **Step 4: Check git status**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git --git-dir=.git-real --work-tree=. status --short --branch
|
||||
git --git-dir=.git-real --work-tree=. log -5 --oneline
|
||||
```
|
||||
|
||||
Expected: branch contains the three implementation commits from this plan and no unstaged edits.
|
||||
|
||||
- [ ] **Step 5: Push the branch**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git --git-dir=.git-real --work-tree=. push
|
||||
```
|
||||
|
||||
Expected: push succeeds to `origin/hardcore-interaction-expansion`.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: Task 1 covers seed vocabulary, aliases, seed-lock config, seed trace, seed-control UI, and tooltips. Task 2 covers normal prompt clothing mode routing, primary softcore outfit routing, partner outfit routing, and legacy content fallback. Task 3 covers scene layer mappings, wardrobe axis behavior, softcore branch clothing-only rerolls, hard-branch continuity, and `content_pose` not touching clothing. Task 4 covers compilation, focused smoke tests, full smoke, status, and push.
|
||||
- Scope check: this plan avoids rewriting category data and keeps custom content item selection on the `content` axis, matching the spec's out-of-scope section.
|
||||
- Type consistency: all new seed keys use `clothing_seed`; all new axis names use `clothing`, `content_clothing`, and `clothing_pose`; compatibility aliases use `outfit_seed`, `wardrobe_seed`, and fallback `content_seed`.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Scene Layer Seed Simplification Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make `SxCPSceneLayerSeedOptions` use the visible node seed for `random` mode so prompt clothing/content choices are reproducible from the workflow.
|
||||
|
||||
**Architecture:** Keep the existing scene seed pipeline. Change only layer seed option resolution so `random` no longer replaces the visible seed with a hidden `SystemRandom` value. Preserve existing `seed_trace` and scene pair metadata behavior.
|
||||
|
||||
**Tech Stack:** Python ComfyUI custom nodes, existing smoke tests in `tools/prompt_smoke.py`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Failing Smoke Coverage
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/prompt_smoke.py`
|
||||
|
||||
- [ ] **Step 1: Add assertions to `smoke_node_scene_chain_registration`**
|
||||
|
||||
Add a small check after the node registry assertions or near the existing scene layer seed checks:
|
||||
|
||||
```python
|
||||
random_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
|
||||
"softcore_branch",
|
||||
"random",
|
||||
123456789,
|
||||
"content_pose",
|
||||
"same_for_all_rows",
|
||||
"replace_layer",
|
||||
)[0]
|
||||
fixed_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
|
||||
"softcore_branch",
|
||||
"fixed",
|
||||
123456789,
|
||||
"content_pose",
|
||||
"same_for_all_rows",
|
||||
"replace_layer",
|
||||
)[0]
|
||||
random_seed_item = json.loads(random_seed_options)["items"][0]
|
||||
fixed_seed_item = json.loads(fixed_seed_options)["items"][0]
|
||||
_expect(random_seed_item.get("seed") == 123456789, "Scene random layer seed should use the visible node seed")
|
||||
_expect(
|
||||
random_seed_item.get("seed") == fixed_seed_item.get("seed"),
|
||||
"Scene random and fixed layer seeds should match when the visible seed matches",
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run focused smoke test and verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
|
||||
```
|
||||
|
||||
Expected: FAIL with `Scene random layer seed should use the visible node seed`.
|
||||
|
||||
### Task 2: Make Visible Seed Authoritative
|
||||
|
||||
**Files:**
|
||||
- Modify: `node_scene.py`
|
||||
|
||||
- [ ] **Step 1: Remove hidden random replacement**
|
||||
|
||||
In `_layer_seed_options_json`, remove the `SystemRandom` override:
|
||||
|
||||
```python
|
||||
resolved_seed = max(0, min(0xFFFFFFFF, int(seed)))
|
||||
```
|
||||
|
||||
The `if seed_mode == "random": ...` branch should be deleted. Leave `seed_mode` in metadata unchanged for compatibility.
|
||||
|
||||
- [ ] **Step 2: Run focused smoke test and verify it passes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
### Task 3: Verify and Commit
|
||||
|
||||
**Files:**
|
||||
- Verify: `node_scene.py`
|
||||
- Verify: `tools/prompt_smoke.py`
|
||||
- Commit: implementation and plan file
|
||||
|
||||
- [ ] **Step 1: Compile changed Python files**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python -m py_compile node_scene.py tools/prompt_smoke.py
|
||||
```
|
||||
|
||||
Expected: exit code 0.
|
||||
|
||||
- [ ] **Step 2: Run full smoke suite**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --quiet
|
||||
```
|
||||
|
||||
Expected: either all smoke cases pass, or only the existing unrelated `krea2_prompt_guide_policy` methodology-memory failure remains.
|
||||
|
||||
- [ ] **Step 3: Review diff**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git --git-dir=.git-real --work-tree=. diff --stat
|
||||
git --git-dir=.git-real --work-tree=. diff -- node_scene.py tools/prompt_smoke.py
|
||||
```
|
||||
|
||||
Expected: only the random seed behavior and its smoke coverage changed.
|
||||
|
||||
- [ ] **Step 4: Commit implementation**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git --git-dir=.git-real --work-tree=. add node_scene.py tools/prompt_smoke.py docs/superpowers/plans/2026-07-01-scene-layer-seed-simplification.md
|
||||
git --git-dir=.git-real --work-tree=. commit -m "Make scene layer random seeds reproducible"
|
||||
```
|
||||
@@ -0,0 +1,136 @@
|
||||
# Clothing Seed Axis Design
|
||||
|
||||
## Goal
|
||||
|
||||
Allow a workflow to keep the same category, content item, pose, role, person,
|
||||
scene, expression, composition, and branch structure while rerolling only the
|
||||
clothing/outfit choices.
|
||||
|
||||
## Current Problem
|
||||
|
||||
Clothing currently shares the `content` seed axis in several places. That means a
|
||||
user cannot change clothes without also risking changes to content-driven prompt
|
||||
selection. In scene pairs, this is especially confusing because a softcore branch
|
||||
content seed can pick the outfit that the hardcore branch later inherits through
|
||||
clothing continuity.
|
||||
|
||||
The previous layer-seed fix made the visible seed authoritative, but it did not
|
||||
separate clothing from content. A visible `content` seed is still doing two jobs:
|
||||
content selection and clothing selection.
|
||||
|
||||
## Design
|
||||
|
||||
Add a new first-class `clothing` seed axis.
|
||||
|
||||
The `clothing` axis controls:
|
||||
|
||||
- prompt clothing mode selection, such as full, minimal, or random;
|
||||
- clothing/outfit item selection for normal prompt rows;
|
||||
- softcore branch outfit selection;
|
||||
- pair partner outfit selection;
|
||||
- scene wardrobe outfit selection where the wardrobe layer needs a seed;
|
||||
- hard branch clothing continuity when it derives from the soft branch outfit.
|
||||
|
||||
The `content` axis continues to control:
|
||||
|
||||
- category item selection;
|
||||
- content templates and item text;
|
||||
- content-derived item axis values;
|
||||
- pose-category content when an existing category treats the content item as
|
||||
pose-driven metadata.
|
||||
|
||||
Changing only `clothing_seed` should not change non-clothing prompt content.
|
||||
|
||||
## Seed Vocabulary
|
||||
|
||||
Update the shared seed policy:
|
||||
|
||||
- Add `clothing` to `SEED_LOCK_AXES`.
|
||||
- Add a deterministic salt for `clothing`.
|
||||
- Add aliases for `clothing_seed`, `outfit_seed`, and `wardrobe_seed`.
|
||||
- Keep `item_seed` as a `content` alias because it describes content item
|
||||
selection, not clothing.
|
||||
- Add `clothing` to reroll-axis choices.
|
||||
- Add `content_clothing` for workflows that intentionally reroll both content
|
||||
and clothes together.
|
||||
- Add `clothing_pose` for workflows that keep content stable but reroll clothes,
|
||||
pose, and role together.
|
||||
|
||||
`content_pose` should remain content + pose + role. It should not include
|
||||
clothing after this split.
|
||||
|
||||
## Scene Layer Behavior
|
||||
|
||||
Update scene layer seed mappings:
|
||||
|
||||
- `wardrobe` defaults to the `clothing` axis.
|
||||
- `softcore_branch` defaults to `clothing`, `pose`, and `role` only when no
|
||||
explicit reroll axis is selected.
|
||||
- `hardcore_branch` remains `pose` and `role` by default.
|
||||
- Explicit reroll axes use the shared vocabulary, including `clothing`,
|
||||
`content_clothing`, and `clothing_pose`.
|
||||
|
||||
This keeps the branch seed UI useful while making clothes-only rerolls obvious.
|
||||
|
||||
## Prompt Flow
|
||||
|
||||
Normal prompt rows should use separate RNGs:
|
||||
|
||||
- `content_rng` for category/subcategory item and content-template choices.
|
||||
- `clothing_rng` for clothing mode and outfit choices.
|
||||
- Existing pose, role, person, scene, expression, and composition RNGs remain
|
||||
unchanged.
|
||||
|
||||
Scene pair rows should use separate RNGs:
|
||||
|
||||
- soft branch clothing/outfit selection uses the soft branch `clothing` axis;
|
||||
- soft branch pose uses the soft branch `pose` axis;
|
||||
- hard branch content behavior remains independent from soft branch clothing;
|
||||
- hard branch clothing continuity reads the selected soft branch outfit, so a
|
||||
clothing-only reroll changes inherited clothing but leaves the hard branch pose
|
||||
and content seeds alone.
|
||||
|
||||
## Compatibility
|
||||
|
||||
Existing workflows should continue to load.
|
||||
|
||||
Compatibility rules:
|
||||
|
||||
- Existing configs without `clothing_seed` fall back to the same effective base
|
||||
seed behavior as before.
|
||||
- `outfit_seed` should map to `clothing` going forward.
|
||||
- `content_seed` should not control clothing once `clothing_seed` or
|
||||
`outfit_seed` is present.
|
||||
- The visible scene layer seed remains authoritative for all seed modes.
|
||||
- `seed_trace` should include `clothing` when the clothing axis is configured or
|
||||
emitted by row generation.
|
||||
|
||||
This is a behavior improvement, not a schema break.
|
||||
|
||||
## Testing
|
||||
|
||||
Add smoke coverage proving:
|
||||
|
||||
- `seed_reroll_axis_choices()` includes `clothing`, `content_clothing`, and
|
||||
`clothing_pose`.
|
||||
- `build_seed_lock_config_json(..., reroll_axis="clothing")` changes only
|
||||
`clothing_seed`.
|
||||
- `build_seed_lock_config_json(..., reroll_axis="content_clothing")` changes
|
||||
both `content_seed` and `clothing_seed`.
|
||||
- `content_pose` does not change `clothing_seed`.
|
||||
- In a scene pair, changing only the soft branch clothing seed changes the
|
||||
softcore outfit and inherited hard clothing state.
|
||||
- The same scene pair keeps non-clothing content, pose, role, person, scene, and
|
||||
composition seeds stable under a clothes-only reroll.
|
||||
- Legacy `outfit_seed` is honored as a clothing seed.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
This design does not add a second visible seed widget to the existing scene layer
|
||||
seed node. The node still has one visible seed field; the selected reroll axis
|
||||
decides what that seed controls.
|
||||
|
||||
This design does not rewrite all prompt category data to distinguish fashion
|
||||
categories from non-fashion content categories. It only separates RNG control
|
||||
for clothing choices that the code already treats as clothing or outfit
|
||||
selection.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Scene Layer Seed Design
|
||||
|
||||
## Problem
|
||||
|
||||
`SxCPSceneLayerSeedOptions` currently has a confusing double-seed behavior. When
|
||||
`seed_mode=random`, the node displays one seed in the widget, but the build
|
||||
method replaces it with a hidden `SystemRandom` value. The generated prompt may
|
||||
therefore use a clothing/content seed that is not visible in the workflow after
|
||||
the run.
|
||||
|
||||
This makes softcore branch clothing hard to reproduce. In a scene pair, the
|
||||
woman's softcore outfit is selected from the softcore branch content seed, then
|
||||
the hardcore branch may inherit that outfit through clothing continuity. If the
|
||||
resolved content seed is hidden, the user cannot reliably answer which seed
|
||||
picked the clothes.
|
||||
|
||||
## Approved Behavior
|
||||
|
||||
The visible `seed` field in `SxCPSceneLayerSeedOptions` is the authoritative
|
||||
seed. `seed_mode=random` must no longer replace it with a hidden random value.
|
||||
For layer-seed behavior:
|
||||
|
||||
- `follow_global`: use the scene seed.
|
||||
- `fixed`: use the visible node `seed`.
|
||||
- `random`: use the visible node `seed`.
|
||||
- `disabled`: apply no layer seed.
|
||||
|
||||
The `random` option can remain in the UI for compatibility, but it behaves like
|
||||
an explicit seed mode. If a user wants a new random value, they should randomize
|
||||
the visible seed field in the node or use ComfyUI's widget randomization.
|
||||
|
||||
## Seed Reporting
|
||||
|
||||
The existing scene `seed_trace` remains the source of truth for resolved prompt
|
||||
axis seeds. For softcore branch clothing, the relevant trace is usually:
|
||||
|
||||
- layer: `softcore_branch`
|
||||
- reroll axis: `content` or `content_pose`
|
||||
- affected axes: `content_seed`, and for `content_pose` also `pose_seed` and
|
||||
`role_seed`
|
||||
|
||||
Scene pair metadata should continue to include the full softcore and hardcore
|
||||
scene chain so `seed_trace` is preserved in `metadata_json` and
|
||||
`scene_metadata_json`.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
This change does not introduce a separate `clothing_seed` axis. Clothing is still
|
||||
part of the existing content axis. Splitting clothing from content would be a
|
||||
larger behavior change and should be handled separately if needed.
|
||||
|
||||
This change does not alter image sampler seeds. KSampler/image seeds remain
|
||||
separate from prompt layer seeds.
|
||||
|
||||
## Testing
|
||||
|
||||
Add or update smoke coverage for `SxCPSceneLayerSeedOptions`:
|
||||
|
||||
- A `random` mode layer seed emits the visible widget seed in its metadata.
|
||||
- A `fixed` mode layer seed emits the same seed as `random` when given the same
|
||||
visible seed.
|
||||
- A softcore branch `content` or `content_pose` seed appears in the generated
|
||||
softcore row `seed_config`.
|
||||
- The generated softcore outfit can be traced to that visible content seed.
|
||||
|
||||
Existing scene pair tests should continue to verify that softcore branch seeds do
|
||||
not leak into hardcore pose/content seeds unless explicitly applied to the hard
|
||||
branch.
|
||||
@@ -0,0 +1,266 @@
|
||||
# SxCP Eval Loop
|
||||
|
||||
This loop is for tuning the SxCP generator toward stronger Krea2 images.
|
||||
ComfyUI sends a generated prompt, image, and seed to Codex, Codex analyzes the
|
||||
result, then sends back exactly one edited prompt for the next A/B test.
|
||||
Confirmed findings become either generator changes or durable prompt rules in
|
||||
[`krea2-prompt-guide.md`](krea2-prompt-guide.md).
|
||||
The active A/B testing method is recorded in
|
||||
[`krea2-ab-methodology.md`](krea2-ab-methodology.md); update that memory when
|
||||
the method improves.
|
||||
|
||||
## Channels
|
||||
|
||||
- `sxcp_eval_in`: ComfyUI to Codex. Contains the prompt text, image path, and
|
||||
seed.
|
||||
- `sxcp_eval_out`: Codex to ComfyUI. Prompt-only text plus the same seed through
|
||||
the MCP signal when supported. Do not put analysis here.
|
||||
- `sxcp_eval_log`: optional analysis/log channel.
|
||||
|
||||
## MCP Helper Command
|
||||
|
||||
Use the checked helper for bridge calls instead of ad hoc Python snippets. The
|
||||
approved command prefix is:
|
||||
|
||||
```bash
|
||||
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py
|
||||
```
|
||||
|
||||
Common calls:
|
||||
|
||||
```bash
|
||||
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py list-tools
|
||||
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py call-tool comfy_pull --arguments-json '{"channel":"sxcp_eval_in"}'
|
||||
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py call-tool comfy_push --arguments-json '{"channel":"sxcp_eval_out","seed":5656565656,"text":"PROMPT_ONLY_POSITIVE_CONDITIONING"}'
|
||||
```
|
||||
|
||||
## Batch Prompt Helper
|
||||
|
||||
For prompt-axis batches, prepare a local JSON file and use the offline helper to
|
||||
render the approved MCP push/pull commands and an image-presence checklist:
|
||||
|
||||
```bash
|
||||
python tools/sxcp_prompt_batch.py validate --batch-json /tmp/sxcp-batch.json
|
||||
python tools/sxcp_prompt_batch.py print-push-commands --batch-json /tmp/sxcp-batch.json
|
||||
python tools/sxcp_prompt_batch.py print-result-template --batch-json /tmp/sxcp-batch.json
|
||||
python tools/sxcp_prompt_batch.py run-batch --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json --previous-turn 80 --run
|
||||
python tools/sxcp_prompt_batch.py validate-results --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json
|
||||
python tools/sxcp_prompt_batch.py print-eval-entry-draft --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json --variant-key pov_example_variant --baseline-image /absolute/baseline.png --candidate-id controlled_subject_first
|
||||
```
|
||||
|
||||
Batch files use the fixed sampler seed and one positive prompt per probe:
|
||||
|
||||
```json
|
||||
{
|
||||
"seed": 8989898989,
|
||||
"channel_out": "sxcp_eval_out",
|
||||
"channel_in": "sxcp_eval_in",
|
||||
"probes": [
|
||||
{
|
||||
"id": "controlled_subject_first",
|
||||
"prompt_order": "subject_first",
|
||||
"text": "SUBJECT_LOOK_FIRST. POSE_HIERARCHY. LOCATION_ANCHORS."
|
||||
},
|
||||
{
|
||||
"id": "rough_geometry_axis",
|
||||
"prompt_order": "geometry_only",
|
||||
"text": "POSE_AXIS_ONLY_FOR_DISCOVERY."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`geometry_only` probes are for rough pose-axis discovery and are not durable
|
||||
subject/look-controlled A/B evidence. The helper rejects
|
||||
`sxcp_eval_negative_out`; keep batch prompts positive-only.
|
||||
|
||||
Use `run-batch --run` to push one positive prompt, poll `sxcp_eval_in` until a
|
||||
new turn and absolute PNG image path appear with the fixed sampler seed, write
|
||||
the filled result JSON, then send the next probe. Omit `--run` for a dry-run
|
||||
command preview. After a live run, run `validate-results`; it requires the
|
||||
result probe ids to match the batch order, each turn to advance in batch order,
|
||||
every image path to be an absolute PNG artifact, and every returned seed to
|
||||
match the fixed sampler seed. Then use `print-eval-entry-draft` to create a
|
||||
valid `krea2-eval-log.json` entry draft. Replace the generated summaries and
|
||||
observation with the real visual comparison before recording it with
|
||||
`tools/krea2_record_eval.py`. By default the draft command rejects
|
||||
`geometry_only` candidates; pass `--allow-geometry-only` only when deliberately
|
||||
recording non-controlled prompt-axis evidence.
|
||||
|
||||
## Manual Loop
|
||||
|
||||
Start the helper after sending a test prompt:
|
||||
|
||||
```bash
|
||||
tools/sxcp_eval_loop.sh 3
|
||||
```
|
||||
|
||||
Every three minutes it prints a structured request asking Codex to:
|
||||
|
||||
1. Pull `sxcp_eval_in`.
|
||||
2. Record the emitted seed.
|
||||
3. Inspect the image.
|
||||
4. Compare it to the prompt and previous edit.
|
||||
5. Push one prompt-only edit to `sxcp_eval_out`, preserving the same seed through
|
||||
the MCP signal when available.
|
||||
6. Classify the finding as prompt-only, prompt-guide rule, provisional generator
|
||||
improvement, or proven generator fix.
|
||||
7. When leaving a category after same-seed progress over baseline, mirror the
|
||||
best generator-safe wording into the responsible generator path as
|
||||
`provisional_generator_patch`.
|
||||
8. Promote a generator change to proven only when the issue is systemic,
|
||||
repeated, or structurally wrong before rendering.
|
||||
9. Record the finding and update the Krea2 prompt guide when a rule is confirmed.
|
||||
|
||||
Runtime logs are written under `.sxcp_eval/` and ignored by git.
|
||||
|
||||
Durable fixed-seed findings that justify a guide rule, generator patch, or pose
|
||||
variant promotion are recorded in [`krea2-eval-log.json`](krea2-eval-log.json).
|
||||
Method changes belong in [`krea2-ab-methodology.md`](krea2-ab-methodology.md).
|
||||
Use runtime logs for scratch notes; use the JSON log only for evidence that
|
||||
should remain tied to a catalog variant. Image paths in that log point at
|
||||
external ComfyUI artifacts and may be cleaned; the durable evidence is the fixed
|
||||
sampler seed, optional generator seed, prompt summaries, observation, decision,
|
||||
and commit.
|
||||
|
||||
Record durable findings with the checked helper instead of hand-editing the log:
|
||||
|
||||
```bash
|
||||
python tools/krea2_record_eval.py --print-template --variant-key pov_footjob_frontal_sole_stroke --seed 1234 --generator-seed 5678 > /tmp/krea2-entry.json
|
||||
python tools/krea2_record_eval.py --entry-json /tmp/krea2-entry.json --dry-run
|
||||
python tools/krea2_record_eval.py --entry-json /tmp/krea2-entry.json
|
||||
```
|
||||
|
||||
Entry template:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "variant-seed-short-finding",
|
||||
"date": "2026-06-29",
|
||||
"variant_key": "pov_example_variant",
|
||||
"seed": 1234,
|
||||
"generator_seed": 5678,
|
||||
"source": "sxcp_eval_mcp",
|
||||
"result": "accepted",
|
||||
"decision": "generator_patch",
|
||||
"baseline_prompt_summary": "What the generated prompt did before the edit.",
|
||||
"candidate_prompt_summary": "What the edited prompt changed for the same seed.",
|
||||
"observation": "What the image comparison proved and why it matters for the generator or guide.",
|
||||
"baseline_image": "/absolute/path/to/baseline.png",
|
||||
"candidate_image": "/absolute/path/to/candidate.png",
|
||||
"commit": "pending"
|
||||
}
|
||||
```
|
||||
|
||||
To see catalog coverage and the next variants that still need controlled
|
||||
testing, run:
|
||||
|
||||
```bash
|
||||
python tools/krea2_tuning_report.py
|
||||
```
|
||||
|
||||
The report includes atlas references plus prompt cues and avoid cues for the
|
||||
next fixed-seed test candidate. It also shows the latest durable evidence for
|
||||
variants that already have fixed-seed results, including the evidence id, seed,
|
||||
decision, candidate prompt summary, and observation. For each normal next-test
|
||||
candidate, it prints a `krea2_record_eval.py --print-template` command; replace
|
||||
`<fixed_seed>` with the seed from the run you are recording.
|
||||
|
||||
## Optional Command Hook
|
||||
|
||||
If you have a one-shot Codex command you want to run automatically, set:
|
||||
|
||||
```bash
|
||||
SXCP_EVAL_CODEX_CMD="codex exec" tools/sxcp_eval_loop.sh 3
|
||||
```
|
||||
|
||||
The request is sent on stdin. The command also receives:
|
||||
|
||||
- `SXCP_EVAL_IN_CHANNEL`
|
||||
- `SXCP_EVAL_OUT_CHANNEL`
|
||||
- `SXCP_EVAL_LOG_CHANNEL`
|
||||
- `SXCP_EVAL_GUIDE_FILE`
|
||||
- `SXCP_EVAL_REQUEST_FILE`
|
||||
- `SXCP_EVAL_CYCLE_DIR`
|
||||
- `SXCP_EVAL_CYCLE`
|
||||
|
||||
## Evaluation Axes
|
||||
|
||||
- Identity consistency
|
||||
- Outfit continuity
|
||||
- Pose/action accuracy
|
||||
- Camera compliance
|
||||
- Location coherence
|
||||
- Crop/framing
|
||||
- Prompt noise/repetition
|
||||
- Model confusion tokens
|
||||
- Seed control/reproducibility
|
||||
- Overall Krea2 image usefulness
|
||||
|
||||
## POV Pose Atlas
|
||||
|
||||
Use `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2` as the local
|
||||
reference atlas for POV pose geometry. The top-level pose folders contain real
|
||||
POV examples, and matching `_control` folders contain solo/control versions.
|
||||
Ignore `bg` and `*_bg` folders for pose rules; they are background plates
|
||||
without people. Treat the pose image folders as the primary source for body
|
||||
geometry; captions are optional and are not present for every folder.
|
||||
|
||||
Suggested workflow:
|
||||
|
||||
1. Choose one pose family, for example `doggy`, `doggy_alt`, `cowgirl`, or
|
||||
`missionary`.
|
||||
2. Sample 5-10 real pose images and their control images.
|
||||
3. Write the repeated geometry as a compact prompt rule.
|
||||
4. Run one fixed-seed Krea2 prompt using that rule.
|
||||
5. Repeat on a second seed or character before changing generator defaults.
|
||||
6. If the prompt itself is structurally contradictory before rendering, patch
|
||||
immediately and add a regression test.
|
||||
|
||||
For POV doggy, the atlas shows that visible viewer thighs, lower torso, or
|
||||
pelvis can be correct. Do not treat them as automatic failures.
|
||||
|
||||
## Seed Contract
|
||||
|
||||
The sampler seed is transport metadata, not prompt text. When the graph emits a
|
||||
sampler seed, an A/B wording test should reuse that exact seed so the image
|
||||
difference mostly comes from wording, not sampling randomness. If the SxCP
|
||||
generator/control seed differs from the sampler seed, record it as
|
||||
`generator_seed` in the eval entry. If a payload has no sampler seed, mark that
|
||||
cycle as uncontrolled and avoid turning the result into a durable generator rule
|
||||
without another controlled run.
|
||||
|
||||
## Positive-Only Conditioning
|
||||
|
||||
`sxcp_eval_out` is positive conditioning only. Never send negative-conditioning
|
||||
phrases such as `no shaft`, `no hands`, `without clothing`, or `avoid X` inside
|
||||
the positive prompt; distilled Krea2 can reinforce or hallucinate the unwanted
|
||||
object from that wording.
|
||||
|
||||
This loop has no active negative-output channel. A same-positive, same-seed
|
||||
probe on seed `424242` compared empty negative conditioning against strong
|
||||
negative text targeting visible prompt attributes, and the rendered image stayed
|
||||
visually unchanged. Do not rely on negative conditioning for Krea2 pose tuning;
|
||||
keep prompt fixes positive-only.
|
||||
|
||||
## Generator Fix Rule
|
||||
|
||||
Use two levels of generator change:
|
||||
|
||||
- `provisional_generator_patch`: apply the best generator-safe wording when
|
||||
leaving a category after fixed-seed progress over baseline. Keep the catalog
|
||||
variant as `candidate`.
|
||||
- `generator_patch`: promote as a proven/default generator rule when the issue
|
||||
is repeated, systemic, or structurally wrong before rendering.
|
||||
|
||||
Examples of proven generator fixes:
|
||||
|
||||
- Selfie wording overrides orbit camera.
|
||||
- Clothing continuity loses the selected softcore outfit.
|
||||
- POV wording makes the off-camera participant the visual subject.
|
||||
- Location camera layout inserts foreground anchors in the wrong place.
|
||||
|
||||
For one-off model drift inside an active category, send a cleaner prompt to
|
||||
`sxcp_eval_out` and keep collecting evidence. When exiting a category, carry
|
||||
forward same-seed improvements over baseline as provisional generator changes
|
||||
and add the rule or weak case to `docs/krea2-prompt-guide.md`.
|
||||
@@ -0,0 +1,960 @@
|
||||
{
|
||||
"last_node_id": 45,
|
||||
"last_link_id": 56,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "SxCPGlobalSeed",
|
||||
"pos": [-1900, -1040],
|
||||
"size": [300, 90],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "seed", "type": "INT", "links": null, "slot_index": 0},
|
||||
{"name": "seed_config", "type": "STRING", "links": [1], "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPGlobalSeed"},
|
||||
"widgets_values": [20260821]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "SxCPSceneStart",
|
||||
"pos": [-1260, -600],
|
||||
"size": [360, 250],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "seed_config", "type": "STRING", "link": 1}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [2], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneStart"},
|
||||
"widgets_values": [1, 41, 20260821, "raw", "provocative_erotic", "random", "balanced", "sxcppnl7", true]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "SxCPSceneCastOptions",
|
||||
"pos": [-840, -1040],
|
||||
"size": [340, 180],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "cast_options", "type": "STRING", "links": [11], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneCastOptions"},
|
||||
"widgets_values": ["replace", "mixed_couple", 1, 1, "woman_a", "none"]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "SxCPSceneCast",
|
||||
"pos": [-840, -600],
|
||||
"size": [360, 150],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 2},
|
||||
{"name": "cast_options", "type": "STRING", "link": 11}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [12], "slot_index": 0},
|
||||
{"name": "cast_config", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneCast"},
|
||||
"widgets_values": ["mixed_couple", 1, 1, "woman_a", "none"]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "SxCPSceneCharacterOptions",
|
||||
"pos": [-420, -1040],
|
||||
"size": [390, 260],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "character_options", "type": "STRING", "links": [14], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneCharacterOptions"},
|
||||
"widgets_values": ["replace", "medium", "visible", "enabled", 0.45, 0.35, 0.85, "creator remains visually central"]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "SxCPSceneCharacter",
|
||||
"pos": [-420, -760],
|
||||
"size": [390, 360],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 12},
|
||||
{"name": "character_options", "type": "STRING", "link": 14},
|
||||
{"name": "seed_options", "type": "STRING", "link": 9}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [15], "slot_index": 0},
|
||||
{"name": "character_cast", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "character_slot", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 3},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 4}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneCharacter"},
|
||||
"widgets_values": [true, "woman", "A", -1, "25-year-old adult", "random", "random", "random", "medium", true, 0.45, "visible", -1.0, -1.0]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "SxCPSceneCharacter",
|
||||
"pos": [-420, -310],
|
||||
"size": [390, 360],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 15},
|
||||
{"name": "seed_options", "type": "STRING", "link": 13}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [16], "slot_index": 0},
|
||||
{"name": "character_cast", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "character_slot", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 3},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 4}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneCharacter"},
|
||||
"widgets_values": [true, "man", "A", -1, "40-year-old adult", "random", "random", "average", "compact", true, 0.35, "visible", -1.0, -1.0]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "SxCPSceneWardrobeOptions",
|
||||
"pos": [40, -1040],
|
||||
"size": [410, 320],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "wardrobe_options", "type": "STRING", "links": [18], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneWardrobeOptions"},
|
||||
"widgets_values": ["replace", "woman", "A", "minimal", "explicit_nude", true, "black lace lingerie set with stockings and garter details", "", "thin necklace, small earrings", "softcore outfit only; hardcore branch should not repeat outfit tokens"]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "SxCPSceneWardrobe",
|
||||
"pos": [40, -760],
|
||||
"size": [390, 250],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 16},
|
||||
{"name": "wardrobe_options", "type": "STRING", "link": 18},
|
||||
{"name": "seed_options", "type": "STRING", "link": 17}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [19], "slot_index": 0},
|
||||
{"name": "character_cast", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneWardrobe"},
|
||||
"widgets_values": [true, "woman", "A", "minimal", "black lace lingerie set with stockings and garter details", "fully nude", ""]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "SxCPSceneWardrobeOptions",
|
||||
"pos": [40, 10],
|
||||
"size": [410, 290],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "wardrobe_options", "type": "STRING", "links": [20], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneWardrobeOptions"},
|
||||
"widgets_values": ["replace", "man", "A", "full", "dressed", true, "half-open black shirt with dark trousers", "shirt open, lower body mostly off-camera when explicit action is framed", "", "partner outfit stays simpler than creator outfit"]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "SxCPSceneWardrobe",
|
||||
"pos": [40, -360],
|
||||
"size": [390, 250],
|
||||
"flags": {},
|
||||
"order": 10,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 19},
|
||||
{"name": "wardrobe_options", "type": "STRING", "link": 20},
|
||||
{"name": "seed_options", "type": "STRING", "link": 21}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [22], "slot_index": 0},
|
||||
{"name": "character_cast", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneWardrobe"},
|
||||
"widgets_values": [true, "man", "A", "full", "half-open black shirt with dark trousers", "shirt open, lower body mostly off-camera when explicit action is framed", ""]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "SxCPSceneLocationLayoutOptions",
|
||||
"pos": [500, -1040],
|
||||
"size": [410, 250],
|
||||
"flags": {},
|
||||
"order": 11,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "location_options", "type": "STRING", "links": [23], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneLocationLayoutOptions"},
|
||||
"widgets_values": ["replace", "near desk edge, laptop corner, chair back", "warm work desks, laptop tables, glass partition seams", "repeated desk rows, plants, tall windows", "semi_public", "semi_public", "coworking lounge layout that survives camera rerolls"]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "SxCPSceneLocation",
|
||||
"pos": [500, -760],
|
||||
"size": [410, 210],
|
||||
"flags": {},
|
||||
"order": 12,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 22},
|
||||
{"name": "location_options", "type": "STRING", "link": 23},
|
||||
{"name": "seed_options", "type": "STRING", "link": 24}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [25], "slot_index": 0},
|
||||
{"name": "location_config", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneLocation"},
|
||||
"widgets_values": [true, "replace", "custom_only", "coworking lounge with tall windows, warm desks, glass partitions, plants, and polished office-lounge surfaces", "shared location base"]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "SxCPSceneSetDressingOptions",
|
||||
"pos": [500, -170],
|
||||
"size": [410, 230],
|
||||
"flags": {},
|
||||
"order": 13,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "set_options", "type": "STRING", "links": [26], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneSetDressingOptions"},
|
||||
"widgets_values": ["replace", "laptop corner and polished tabletop line", "desks, chairs, glass seams, plant rows", "phone, coffee cup, folded jacket", "warm wood, glass reflections, muted office light", "keep props beside or behind the action, not blocking the subject"]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "SxCPSceneSetDressing",
|
||||
"pos": [500, -470],
|
||||
"size": [410, 250],
|
||||
"flags": {},
|
||||
"order": 14,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 25},
|
||||
{"name": "set_options", "type": "STRING", "link": 26}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [27], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneSetDressing"},
|
||||
"widgets_values": [true, "", "", "", ""]
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "SxCPSceneBlockingOptions",
|
||||
"pos": [960, -1040],
|
||||
"size": [410, 270],
|
||||
"flags": {},
|
||||
"order": 15,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "blocking_options", "type": "STRING", "links": [28], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneBlockingOptions"},
|
||||
"widgets_values": ["replace", "custom", "woman close to the desk edge with room depth behind her", "man close enough for the hardcore branch but not forced into the softcore pose", "three_quarter", "foreground", "subjects dominate the frame while the coworking pattern remains visible", "shared blocking base before branch-specific action"]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "SxCPSceneBlocking",
|
||||
"pos": [960, -760],
|
||||
"size": [410, 250],
|
||||
"flags": {},
|
||||
"order": 16,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 27},
|
||||
{"name": "blocking_options", "type": "STRING", "link": 28}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [29], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneBlocking"},
|
||||
"widgets_values": [true, "custom", "", "", ""]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "SxCPSceneActionOptions",
|
||||
"pos": [960, -170],
|
||||
"size": [410, 190],
|
||||
"flags": {},
|
||||
"order": 17,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "action_options", "type": "STRING", "links": [30], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneActionOptions"},
|
||||
"widgets_values": ["replace", "softcore", "softcore_tease", "no_change", "soft branch starts as an outfit tease; hardcore branch replaces action through the action filter"]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "SxCPSceneAction",
|
||||
"pos": [960, -470],
|
||||
"size": [410, 210],
|
||||
"flags": {},
|
||||
"order": 18,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 29},
|
||||
{"name": "action_options", "type": "STRING", "link": 30},
|
||||
{"name": "seed_options", "type": "STRING", "link": 31}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [32], "slot_index": 0},
|
||||
{"name": "hardcore_position_config", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneAction"},
|
||||
"widgets_values": [true, "regular", "no_change", ""]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "SxCPScenePerformanceOptions",
|
||||
"pos": [1420, -1040],
|
||||
"size": [410, 250],
|
||||
"flags": {},
|
||||
"order": 19,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "performance_options", "type": "STRING", "links": [33], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPScenePerformanceOptions"},
|
||||
"widgets_values": ["replace", "enabled", "fixed", 0.55, "camera", "on_body", "posed", "controlled creator-camera expression"]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"type": "SxCPScenePerformance",
|
||||
"pos": [1420, -760],
|
||||
"size": [410, 180],
|
||||
"flags": {},
|
||||
"order": 20,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 32},
|
||||
{"name": "performance_options", "type": "STRING", "link": 33}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [34], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPScenePerformance"},
|
||||
"widgets_values": [true, "fixed", 0.55, ""]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"type": "SxCPSceneCameraOptions",
|
||||
"pos": [1420, -150],
|
||||
"size": [410, 170],
|
||||
"flags": {},
|
||||
"order": 21,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "camera_options", "type": "STRING", "links": [35], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneCameraOptions"},
|
||||
"widgets_values": ["replace", "from_camera_config", true, "camera should adapt to the location layout instead of replacing it"]
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"type": "SxCPSceneCamera",
|
||||
"pos": [1420, -530],
|
||||
"size": [410, 330],
|
||||
"flags": {},
|
||||
"order": 22,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 34},
|
||||
{"name": "camera_options", "type": "STRING", "link": 35}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [36], "slot_index": 0},
|
||||
{"name": "camera_config", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneCamera"},
|
||||
"widgets_values": [true, "standard", "three_quarter", "eye_level", "auto", "auto", "vertical_story", "auto", "strong", "compact", ""]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"type": "SxCPSceneCompositionOptions",
|
||||
"pos": [1880, -1040],
|
||||
"size": [410, 200],
|
||||
"flags": {},
|
||||
"order": 23,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "composition_options", "type": "STRING", "links": [37], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneCompositionOptions"},
|
||||
"widgets_values": ["replace", "body", "three_quarter", "clear", "subject remains dominant while desk rows and glass seams prove the location"]
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"type": "SxCPSceneComposition",
|
||||
"pos": [1880, -760],
|
||||
"size": [410, 210],
|
||||
"flags": {},
|
||||
"order": 24,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 36},
|
||||
{"name": "composition_options", "type": "STRING", "link": 37}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [38], "slot_index": 0},
|
||||
{"name": "composition_config", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneComposition"},
|
||||
"widgets_values": [true, "replace", "no_outfit_check", "", ""]
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"type": "SxCPSceneLightingOptions",
|
||||
"pos": [1880, -150],
|
||||
"size": [410, 220],
|
||||
"flags": {},
|
||||
"order": 25,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "lighting_options", "type": "STRING", "links": [39], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneLightingOptions"},
|
||||
"widgets_values": ["replace", "window_light", "soft", "medium", "warm", "evening", "soft office-window light with practical lamps in the background"]
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"type": "SxCPSceneLighting",
|
||||
"pos": [1880, -470],
|
||||
"size": [410, 210],
|
||||
"flags": {},
|
||||
"order": 26,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 38},
|
||||
{"name": "lighting_options", "type": "STRING", "link": 39}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [40], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneLighting"},
|
||||
"widgets_values": [true, "auto", "auto", "auto", "auto", ""]
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"type": "SxCPSceneBranchOptions",
|
||||
"pos": [2340, -1040],
|
||||
"size": [390, 180],
|
||||
"flags": {},
|
||||
"order": 27,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "branch_options", "type": "STRING", "links": [41, 44, 47], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneBranchOptions"},
|
||||
"widgets_values": ["replace", "both", "same_creator_same_room", "hybrid", "same cast, same coworking room, different explicitness"]
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"type": "SxCPSceneBranchPair",
|
||||
"pos": [2340, -600],
|
||||
"size": [340, 130],
|
||||
"flags": {},
|
||||
"order": 28,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 40},
|
||||
{"name": "branch_options", "type": "STRING", "link": 41},
|
||||
{"name": "seed_options", "type": "STRING", "link": 42}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "softcore_scene", "type": "STRING", "links": [43], "slot_index": 0},
|
||||
{"name": "hardcore_scene", "type": "STRING", "links": [45], "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneBranchPair"},
|
||||
"widgets_values": ["same_creator_same_room", "hybrid"]
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"type": "SxCPSoftcoreBranchOptions",
|
||||
"pos": [2760, -760],
|
||||
"size": [390, 260],
|
||||
"flags": {},
|
||||
"order": 29,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 43},
|
||||
{"name": "branch_options", "type": "STRING", "link": 44}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [49], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSoftcoreBranchOptions"},
|
||||
"widgets_values": ["same_as_hardcore", "lingerie_tease", true, 0.45, "from_camera_config", "compact", ""]
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"type": "SxCPHardcoreActionFilter",
|
||||
"pos": [2340, -310],
|
||||
"size": [360, 300],
|
||||
"flags": {},
|
||||
"order": 30,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "hardcore_position_config", "type": "STRING", "links": [46], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPHardcoreActionFilter"},
|
||||
"widgets_values": ["penetration_only", false, false, true, false, false, false, false, false, false, false]
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"type": "SxCPHardcoreBranchOptions",
|
||||
"pos": [2760, -350],
|
||||
"size": [390, 360],
|
||||
"flags": {},
|
||||
"order": 31,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 45},
|
||||
{"name": "hardcore_position_config", "type": "STRING", "link": 46},
|
||||
{"name": "branch_options", "type": "STRING", "link": 47},
|
||||
{"name": "seed_options", "type": "STRING", "link": 48}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [50], "slot_index": 0},
|
||||
{"name": "hardcore_position_config", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPHardcoreBranchOptions"},
|
||||
"widgets_values": ["couple", 1, 1, "hardcore", true, 0.85, "explicit_nude", "from_camera_config", "compact", "balanced", ""]
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"type": "SxCPScenePairOutput",
|
||||
"pos": [3220, -600],
|
||||
"size": [430, 290],
|
||||
"flags": {},
|
||||
"order": 32,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "softcore_scene", "type": "STRING", "link": 49},
|
||||
{"name": "hardcore_scene", "type": "STRING", "link": 50}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "softcore_prompt", "type": "STRING", "links": [53], "slot_index": 0},
|
||||
{"name": "hardcore_prompt", "type": "STRING", "links": [54], "slot_index": 1},
|
||||
{"name": "softcore_negative_prompt", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "hardcore_negative_prompt", "type": "STRING", "links": null, "slot_index": 3},
|
||||
{"name": "softcore_caption", "type": "STRING", "links": null, "slot_index": 4},
|
||||
{"name": "hardcore_caption", "type": "STRING", "links": null, "slot_index": 5},
|
||||
{"name": "shared_descriptor", "type": "STRING", "links": null, "slot_index": 6},
|
||||
{"name": "metadata_json", "type": "STRING", "links": [51, 52], "slot_index": 7},
|
||||
{"name": "scene_metadata_json", "type": "STRING", "links": null, "slot_index": 8}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPScenePairOutput"},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"type": "SxCPKrea2Formatter",
|
||||
"pos": [3720, -760],
|
||||
"size": [390, 270],
|
||||
"flags": {},
|
||||
"order": 33,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "metadata_json", "type": "STRING", "link": 51}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "krea_prompt", "type": "STRING", "links": null, "slot_index": 0},
|
||||
{"name": "negative_prompt", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "krea_softcore_prompt", "type": "STRING", "links": [55], "slot_index": 2},
|
||||
{"name": "krea_hardcore_prompt", "type": "STRING", "links": [56], "slot_index": 3},
|
||||
{"name": "softcore_negative_prompt", "type": "STRING", "links": null, "slot_index": 4},
|
||||
{"name": "hardcore_negative_prompt", "type": "STRING", "links": null, "slot_index": 5},
|
||||
{"name": "method", "type": "STRING", "links": null, "slot_index": 6},
|
||||
{"name": "route_trace_json", "type": "STRING", "links": null, "slot_index": 7}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPKrea2Formatter"},
|
||||
"widgets_values": ["", "metadata_json", "auto", "balanced", "preserve", false, "", ""]
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"type": "SxCPCaptionNaturalizer",
|
||||
"pos": [3720, -370],
|
||||
"size": [390, 240],
|
||||
"flags": {},
|
||||
"order": 34,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "metadata_json", "type": "STRING", "link": 52}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "natural_caption", "type": "STRING", "links": null, "slot_index": 0},
|
||||
{"name": "method", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "route_trace_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPCaptionNaturalizer"},
|
||||
"widgets_values": ["", "metadata_json", "training_dense", "balanced", "drop_style_tail", "sxcppnl7", true, "auto"]
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"type": "SxCPPreviewAnyAsText",
|
||||
"pos": [4200, -900],
|
||||
"size": [420, 180],
|
||||
"flags": {},
|
||||
"order": 35,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "value", "type": "*", "link": 53}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "text", "type": "STRING", "links": null, "slot_index": 0}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPPreviewAnyAsText"},
|
||||
"widgets_values": ["Raw softcore prompt preview", "auto", 30000]
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"type": "SxCPPreviewAnyAsText",
|
||||
"pos": [4200, -690],
|
||||
"size": [420, 180],
|
||||
"flags": {},
|
||||
"order": 36,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "value", "type": "*", "link": 54}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "text", "type": "STRING", "links": null, "slot_index": 0}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPPreviewAnyAsText"},
|
||||
"widgets_values": ["Raw hardcore prompt preview", "auto", 30000]
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"type": "SxCPPreviewAnyAsText",
|
||||
"pos": [4200, -480],
|
||||
"size": [420, 180],
|
||||
"flags": {},
|
||||
"order": 37,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "value", "type": "*", "link": 55}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "text", "type": "STRING", "links": null, "slot_index": 0}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPPreviewAnyAsText"},
|
||||
"widgets_values": ["Krea softcore prompt preview", "auto", 30000]
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"type": "SxCPPreviewAnyAsText",
|
||||
"pos": [4200, -270],
|
||||
"size": [420, 180],
|
||||
"flags": {},
|
||||
"order": 38,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "value", "type": "*", "link": 56}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "text", "type": "STRING", "links": null, "slot_index": 0}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPPreviewAnyAsText"},
|
||||
"widgets_values": ["Krea hardcore prompt preview", "auto", 30000]
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"type": "Note",
|
||||
"pos": [-1900, -470],
|
||||
"size": [560, 260],
|
||||
"flags": {},
|
||||
"order": 39,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Proposed adapter layout: the center row is the mental-model chain (scene start -> cast -> characters -> wardrobe -> location -> set -> blocking -> action -> performance -> camera -> composition -> lighting -> branch). Adapter nodes sit above/below the layer they override. The seed adapter bus on the left is chained once and reused by character, wardrobe, location, action, and hardcore branch nodes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"type": "SxCPSceneLayerSeedOptions",
|
||||
"pos": [-1900, -900],
|
||||
"size": [340, 170],
|
||||
"flags": {},
|
||||
"order": 40,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "seed_options", "type": "STRING", "links": [3], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneLayerSeedOptions"},
|
||||
"widgets_values": ["character", "fixed", 20260831, "person", "same_for_all_rows", "replace_layer"]
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"type": "SxCPSceneLayerSeedOptions",
|
||||
"pos": [-1900, -710],
|
||||
"size": [340, 170],
|
||||
"flags": {},
|
||||
"order": 41,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "seed_options", "type": "STRING", "link": 3}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "seed_options", "type": "STRING", "links": [4], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneLayerSeedOptions"},
|
||||
"widgets_values": ["wardrobe", "fixed", 20260832, "content", "same_for_all_rows", "add"]
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"type": "SxCPSceneLayerSeedOptions",
|
||||
"pos": [-1540, -900],
|
||||
"size": [340, 170],
|
||||
"flags": {},
|
||||
"order": 42,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "seed_options", "type": "STRING", "link": 4}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "seed_options", "type": "STRING", "links": [5], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneLayerSeedOptions"},
|
||||
"widgets_values": ["location", "fixed", 20260833, "scene", "same_for_all_rows", "add"]
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"type": "SxCPSceneLayerSeedOptions",
|
||||
"pos": [-1540, -710],
|
||||
"size": [340, 170],
|
||||
"flags": {},
|
||||
"order": 43,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "seed_options", "type": "STRING", "link": 5}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "seed_options", "type": "STRING", "links": [6], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneLayerSeedOptions"},
|
||||
"widgets_values": ["action", "fixed", 20260834, "pose", "same_for_all_rows", "add"]
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"type": "SxCPSceneLayerSeedOptions",
|
||||
"pos": [-1540, -520],
|
||||
"size": [340, 170],
|
||||
"flags": {},
|
||||
"order": 44,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "seed_options", "type": "STRING", "link": 6}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "seed_options", "type": "STRING", "links": [9, 13, 17, 21, 24, 31, 42, 48], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneLayerSeedOptions"},
|
||||
"widgets_values": ["hardcore_branch", "fixed", 20260835, "pose", "same_for_all_rows", "add"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 1, 2, 0, "STRING"],
|
||||
[2, 2, 0, 4, 0, "STRING"],
|
||||
[3, 41, 0, 42, 0, "STRING"],
|
||||
[4, 42, 0, 43, 0, "STRING"],
|
||||
[5, 43, 0, 44, 0, "STRING"],
|
||||
[6, 44, 0, 45, 0, "STRING"],
|
||||
[9, 45, 0, 6, 2, "STRING"],
|
||||
[11, 3, 0, 4, 1, "STRING"],
|
||||
[12, 4, 0, 6, 0, "STRING"],
|
||||
[13, 45, 0, 7, 1, "STRING"],
|
||||
[14, 5, 0, 6, 1, "STRING"],
|
||||
[15, 6, 0, 7, 0, "STRING"],
|
||||
[16, 7, 0, 9, 0, "STRING"],
|
||||
[17, 45, 0, 9, 2, "STRING"],
|
||||
[18, 8, 0, 9, 1, "STRING"],
|
||||
[19, 9, 0, 11, 0, "STRING"],
|
||||
[20, 10, 0, 11, 1, "STRING"],
|
||||
[21, 45, 0, 11, 2, "STRING"],
|
||||
[22, 11, 0, 13, 0, "STRING"],
|
||||
[23, 12, 0, 13, 1, "STRING"],
|
||||
[24, 45, 0, 13, 2, "STRING"],
|
||||
[25, 13, 0, 15, 0, "STRING"],
|
||||
[26, 14, 0, 15, 1, "STRING"],
|
||||
[27, 15, 0, 17, 0, "STRING"],
|
||||
[28, 16, 0, 17, 1, "STRING"],
|
||||
[29, 17, 0, 19, 0, "STRING"],
|
||||
[30, 18, 0, 19, 1, "STRING"],
|
||||
[31, 45, 0, 19, 2, "STRING"],
|
||||
[32, 19, 0, 21, 0, "STRING"],
|
||||
[33, 20, 0, 21, 1, "STRING"],
|
||||
[34, 21, 0, 23, 0, "STRING"],
|
||||
[35, 22, 0, 23, 1, "STRING"],
|
||||
[36, 23, 0, 25, 0, "STRING"],
|
||||
[37, 24, 0, 25, 1, "STRING"],
|
||||
[38, 25, 0, 27, 0, "STRING"],
|
||||
[39, 26, 0, 27, 1, "STRING"],
|
||||
[40, 27, 0, 29, 0, "STRING"],
|
||||
[41, 28, 0, 29, 1, "STRING"],
|
||||
[42, 45, 0, 29, 2, "STRING"],
|
||||
[43, 29, 0, 30, 0, "STRING"],
|
||||
[44, 28, 0, 30, 1, "STRING"],
|
||||
[45, 29, 1, 32, 0, "STRING"],
|
||||
[46, 31, 0, 32, 1, "STRING"],
|
||||
[47, 28, 0, 32, 2, "STRING"],
|
||||
[48, 45, 0, 32, 3, "STRING"],
|
||||
[49, 30, 0, 33, 0, "STRING"],
|
||||
[50, 32, 0, 33, 1, "STRING"],
|
||||
[51, 33, 7, 34, 0, "STRING"],
|
||||
[52, 33, 7, 35, 0, "STRING"],
|
||||
[53, 33, 0, 36, 0, "STRING"],
|
||||
[54, 33, 1, 37, 0, "STRING"],
|
||||
[55, 34, 2, 38, 0, "STRING"],
|
||||
[56, 34, 3, 39, 0, "STRING"]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"title": "Seed Adapter Bus",
|
||||
"bounding": [-1930, -1090, 760, 780],
|
||||
"color": "#4d6b8f",
|
||||
"font_size": 24
|
||||
},
|
||||
{
|
||||
"title": "Main Scene Chain",
|
||||
"bounding": [-1290, -810, 3610, 900],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24
|
||||
},
|
||||
{
|
||||
"title": "Layer Option Adapters",
|
||||
"bounding": [-870, -1090, 3220, 1180],
|
||||
"color": "#6b5a8f",
|
||||
"font_size": 24
|
||||
},
|
||||
{
|
||||
"title": "Softcore/Hardcore Branch Adapters",
|
||||
"bounding": [2310, -1090, 880, 1160],
|
||||
"color": "#8a5a5a",
|
||||
"font_size": 24
|
||||
},
|
||||
{
|
||||
"title": "Pair Output, Formatters, Persistent Text Previews",
|
||||
"bounding": [3190, -950, 1470, 900],
|
||||
"color": "#4d7f45",
|
||||
"font_size": 24
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.52,
|
||||
"offset": [1290, 665]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
{
|
||||
"last_node_id": 25,
|
||||
"last_link_id": 24,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "SxCPGlobalSeed",
|
||||
"pos": [-1900, -760],
|
||||
"size": [300, 90],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "seed", "type": "INT", "links": null, "slot_index": 0},
|
||||
{"name": "seed_config", "type": "STRING", "links": [1], "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPGlobalSeed"},
|
||||
"widgets_values": [20260801]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "SxCPSceneStart",
|
||||
"pos": [-1540, -840],
|
||||
"size": [360, 250],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "seed_config", "type": "STRING", "link": 1}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [2], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneStart"},
|
||||
"widgets_values": [1, 41, 20260801, "raw", "provocative_erotic", "random", "balanced", "sxcppnl7", true]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "SxCPSceneCast",
|
||||
"pos": [-1540, -520],
|
||||
"size": [360, 150],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 2}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [3], "slot_index": 0},
|
||||
{"name": "cast_config", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneCast"},
|
||||
"widgets_values": ["mixed_couple", 1, 1, "woman_a", "none"]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "SxCPSceneCharacter",
|
||||
"pos": [-1120, -860],
|
||||
"size": [390, 360],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 3}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [4], "slot_index": 0},
|
||||
{"name": "character_cast", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "character_slot", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 3},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 4}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneCharacter"},
|
||||
"widgets_values": [true, "woman", "A", -1, "25-year-old adult", "random", "random", "random", "medium", true, 0.45, "visible", -1.0, -1.0]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "SxCPSceneCharacter",
|
||||
"pos": [-1120, -430],
|
||||
"size": [390, 360],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 4}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [5], "slot_index": 0},
|
||||
{"name": "character_cast", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "character_slot", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 3},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 4}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneCharacter"},
|
||||
"widgets_values": [true, "man", "A", -1, "40-year-old adult", "random", "random", "average", "compact", true, 0.35, "visible", -1.0, -1.0]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "SxCPSceneWardrobe",
|
||||
"pos": [-670, -860],
|
||||
"size": [390, 250],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 5}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [6], "slot_index": 0},
|
||||
{"name": "character_cast", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneWardrobe"},
|
||||
"widgets_values": [true, "woman", "A", "minimal", "black lace lingerie set with stockings and garter details", "fully nude", "base outfit continuity for the creator"]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "SxCPSceneWardrobe",
|
||||
"pos": [-670, -500],
|
||||
"size": [390, 250],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 6}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [7], "slot_index": 0},
|
||||
{"name": "character_cast", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneWardrobe"},
|
||||
"widgets_values": [true, "man", "A", "full", "half-open black shirt with dark trousers", "shirt open, lower body mostly off-camera when explicit action is framed", "partner outfit continuity"]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "SxCPSceneLocation",
|
||||
"pos": [-220, -860],
|
||||
"size": [410, 210],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 7}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [8], "slot_index": 0},
|
||||
{"name": "location_config", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneLocation"},
|
||||
"widgets_values": [true, "replace", "custom_only", "private creator bedroom with bed, mirror, phone tripod, warm lamps, and visible content setup", "same room shared by both branches"]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "SxCPSceneSetDressing",
|
||||
"pos": [-220, -570],
|
||||
"size": [410, 250],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 8}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [9], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneSetDressing"},
|
||||
"widgets_values": [true, "bed edge, mirror frame, phone tripod", "warm lamps, curtains, rumpled bedding", "phone stand, folded clothes nearby", "creator-room set remains readable without forcing camera phrasing"]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "SxCPSceneBlocking",
|
||||
"pos": [250, -860],
|
||||
"size": [410, 250],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 9}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [10], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneBlocking"},
|
||||
"widgets_values": [true, "custom", "woman near the bed and mirror setup", "man close enough for the hardcore branch but not forced into the softcore pose", "shared blocking base for a soft tease or explicit branch"]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "SxCPSceneCamera",
|
||||
"pos": [250, -500],
|
||||
"size": [410, 330],
|
||||
"flags": {},
|
||||
"order": 10,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 10}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [11], "slot_index": 0},
|
||||
{"name": "camera_config", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneCamera"},
|
||||
"widgets_values": [true, "standard", "three_quarter", "eye_level", "auto", "auto", "vertical_story", "auto", "strong", "compact", ""]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "SxCPSceneComposition",
|
||||
"pos": [720, -860],
|
||||
"size": [410, 210],
|
||||
"flags": {},
|
||||
"order": 11,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 11}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [12], "slot_index": 0},
|
||||
{"name": "composition_config", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneComposition"},
|
||||
"widgets_values": [true, "replace", "no_outfit_check", "vertical creator-frame with body and room setup readable", ""]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "SxCPSceneLighting",
|
||||
"pos": [720, -570],
|
||||
"size": [410, 210],
|
||||
"flags": {},
|
||||
"order": 12,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 12}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [13], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneLighting"},
|
||||
"widgets_values": [true, "practical_lamps", "soft", "medium", "warm", ""]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "SxCPSceneBranchPair",
|
||||
"pos": [1190, -720],
|
||||
"size": [340, 120],
|
||||
"flags": {},
|
||||
"order": 13,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 13}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "softcore_scene", "type": "STRING", "links": [14], "slot_index": 0},
|
||||
{"name": "hardcore_scene", "type": "STRING", "links": [15], "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSceneBranchPair"},
|
||||
"widgets_values": ["same_creator_same_room", "hybrid"]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "SxCPSoftcoreBranchOptions",
|
||||
"pos": [1580, -860],
|
||||
"size": [390, 260],
|
||||
"flags": {},
|
||||
"order": 14,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 14}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [17], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPSoftcoreBranchOptions"},
|
||||
"widgets_values": ["same_as_hardcore", "lingerie_tease", true, 0.45, "from_camera_config", "compact", ""]
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "SxCPHardcoreActionFilter",
|
||||
"pos": [1190, -470],
|
||||
"size": [360, 300],
|
||||
"flags": {},
|
||||
"order": 15,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "hardcore_position_config", "type": "STRING", "links": [16], "slot_index": 0},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPHardcoreActionFilter"},
|
||||
"widgets_values": ["penetration_only", false, false, true, false, false, false, false, false, false, false]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "SxCPHardcoreBranchOptions",
|
||||
"pos": [1580, -480],
|
||||
"size": [390, 360],
|
||||
"flags": {},
|
||||
"order": 16,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "scene", "type": "STRING", "link": 15},
|
||||
{"name": "hardcore_position_config", "type": "STRING", "link": 16}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "scene", "type": "STRING", "links": [18], "slot_index": 0},
|
||||
{"name": "hardcore_position_config", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "summary", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "metadata_json", "type": "STRING", "links": null, "slot_index": 3}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPHardcoreBranchOptions"},
|
||||
"widgets_values": ["couple", 1, 1, "hardcore", true, 0.85, "explicit_nude", "from_camera_config", "compact", "balanced", ""]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "SxCPScenePairOutput",
|
||||
"pos": [2050, -720],
|
||||
"size": [430, 290],
|
||||
"flags": {},
|
||||
"order": 17,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "softcore_scene", "type": "STRING", "link": 17},
|
||||
{"name": "hardcore_scene", "type": "STRING", "link": 18}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "softcore_prompt", "type": "STRING", "links": [21], "slot_index": 0},
|
||||
{"name": "hardcore_prompt", "type": "STRING", "links": [22], "slot_index": 1},
|
||||
{"name": "softcore_negative_prompt", "type": "STRING", "links": null, "slot_index": 2},
|
||||
{"name": "hardcore_negative_prompt", "type": "STRING", "links": null, "slot_index": 3},
|
||||
{"name": "softcore_caption", "type": "STRING", "links": null, "slot_index": 4},
|
||||
{"name": "hardcore_caption", "type": "STRING", "links": null, "slot_index": 5},
|
||||
{"name": "shared_descriptor", "type": "STRING", "links": null, "slot_index": 6},
|
||||
{"name": "metadata_json", "type": "STRING", "links": [19, 20], "slot_index": 7},
|
||||
{"name": "scene_metadata_json", "type": "STRING", "links": null, "slot_index": 8}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPScenePairOutput"},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "SxCPKrea2Formatter",
|
||||
"pos": [2550, -820],
|
||||
"size": [390, 270],
|
||||
"flags": {},
|
||||
"order": 18,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "metadata_json", "type": "STRING", "link": 19}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "krea_prompt", "type": "STRING", "links": null, "slot_index": 0},
|
||||
{"name": "negative_prompt", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "krea_softcore_prompt", "type": "STRING", "links": [23], "slot_index": 2},
|
||||
{"name": "krea_hardcore_prompt", "type": "STRING", "links": [24], "slot_index": 3},
|
||||
{"name": "softcore_negative_prompt", "type": "STRING", "links": null, "slot_index": 4},
|
||||
{"name": "hardcore_negative_prompt", "type": "STRING", "links": null, "slot_index": 5},
|
||||
{"name": "method", "type": "STRING", "links": null, "slot_index": 6},
|
||||
{"name": "route_trace_json", "type": "STRING", "links": null, "slot_index": 7}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPKrea2Formatter"},
|
||||
"widgets_values": ["", "metadata_json", "auto", "balanced", "preserve", false, "", ""]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "SxCPCaptionNaturalizer",
|
||||
"pos": [2550, -450],
|
||||
"size": [390, 240],
|
||||
"flags": {},
|
||||
"order": 19,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "metadata_json", "type": "STRING", "link": 20}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "natural_caption", "type": "STRING", "links": null, "slot_index": 0},
|
||||
{"name": "method", "type": "STRING", "links": null, "slot_index": 1},
|
||||
{"name": "route_trace_json", "type": "STRING", "links": null, "slot_index": 2}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPCaptionNaturalizer"},
|
||||
"widgets_values": ["", "metadata_json", "training_dense", "balanced", "drop_style_tail", "sxcppnl7", true, "auto"]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"type": "SxCPPreviewAnyAsText",
|
||||
"pos": [3050, -920],
|
||||
"size": [420, 180],
|
||||
"flags": {},
|
||||
"order": 20,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "value", "type": "*", "link": 21}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "text", "type": "STRING", "links": null, "slot_index": 0}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPPreviewAnyAsText"},
|
||||
"widgets_values": ["Raw softcore prompt preview", "auto", 30000]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"type": "SxCPPreviewAnyAsText",
|
||||
"pos": [3050, -710],
|
||||
"size": [420, 180],
|
||||
"flags": {},
|
||||
"order": 21,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "value", "type": "*", "link": 22}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "text", "type": "STRING", "links": null, "slot_index": 0}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPPreviewAnyAsText"},
|
||||
"widgets_values": ["Raw hardcore prompt preview", "auto", 30000]
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"type": "SxCPPreviewAnyAsText",
|
||||
"pos": [3050, -500],
|
||||
"size": [420, 180],
|
||||
"flags": {},
|
||||
"order": 22,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "value", "type": "*", "link": 23}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "text", "type": "STRING", "links": null, "slot_index": 0}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPPreviewAnyAsText"},
|
||||
"widgets_values": ["Krea softcore prompt preview", "auto", 30000]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"type": "SxCPPreviewAnyAsText",
|
||||
"pos": [3050, -290],
|
||||
"size": [420, 180],
|
||||
"flags": {},
|
||||
"order": 23,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "value", "type": "*", "link": 24}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "text", "type": "STRING", "links": null, "slot_index": 0}
|
||||
],
|
||||
"properties": {"Node name for S&R": "SxCPPreviewAnyAsText"},
|
||||
"widgets_values": ["Krea hardcore prompt preview", "auto", 30000]
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"type": "Note",
|
||||
"pos": [-1900, -560],
|
||||
"size": [520, 210],
|
||||
"flags": {},
|
||||
"order": 24,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Scene-chain Insta/OF demo: build one shared scene, split it with Scene Branch Pair, then refine softcore and hardcore separately before Scene Pair Output. Change pose through Hardcore Action Filter or the seed_config pose/role axes."
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 1, 2, 0, "STRING"],
|
||||
[2, 2, 0, 3, 0, "STRING"],
|
||||
[3, 3, 0, 4, 0, "STRING"],
|
||||
[4, 4, 0, 5, 0, "STRING"],
|
||||
[5, 5, 0, 6, 0, "STRING"],
|
||||
[6, 6, 0, 7, 0, "STRING"],
|
||||
[7, 7, 0, 8, 0, "STRING"],
|
||||
[8, 8, 0, 9, 0, "STRING"],
|
||||
[9, 9, 0, 10, 0, "STRING"],
|
||||
[10, 10, 0, 11, 0, "STRING"],
|
||||
[11, 11, 0, 12, 0, "STRING"],
|
||||
[12, 12, 0, 13, 0, "STRING"],
|
||||
[13, 13, 0, 14, 0, "STRING"],
|
||||
[14, 14, 0, 15, 0, "STRING"],
|
||||
[15, 14, 1, 17, 0, "STRING"],
|
||||
[16, 16, 0, 17, 1, "STRING"],
|
||||
[17, 15, 0, 18, 0, "STRING"],
|
||||
[18, 17, 0, 18, 1, "STRING"],
|
||||
[19, 18, 7, 19, 0, "STRING"],
|
||||
[20, 18, 7, 20, 0, "STRING"],
|
||||
[21, 18, 0, 21, 0, "STRING"],
|
||||
[22, 18, 1, 22, 0, "STRING"],
|
||||
[23, 19, 2, 23, 0, "STRING"],
|
||||
[24, 19, 3, 24, 0, "STRING"]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"title": "Shared scene setup",
|
||||
"bounding": [-1940, -920, 3100, 820],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24
|
||||
},
|
||||
{
|
||||
"title": "Softcore/hardcore branch split",
|
||||
"bounding": [1160, -910, 860, 820],
|
||||
"color": "#5f4d8f",
|
||||
"font_size": 24
|
||||
},
|
||||
{
|
||||
"title": "Pair output, formatters, previews",
|
||||
"bounding": [2020, -960, 1490, 880],
|
||||
"color": "#4d7f45",
|
||||
"font_size": 24
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.62,
|
||||
"offset": [1320, 670]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,265 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
ETHNICITY_FILTER_CHOICES = [
|
||||
"any",
|
||||
"european",
|
||||
"mediterranean_mena",
|
||||
"latina",
|
||||
"east_asian",
|
||||
"southeast_asian",
|
||||
"south_asian",
|
||||
"black_african",
|
||||
"indigenous",
|
||||
"mixed",
|
||||
"asian",
|
||||
"white_asian",
|
||||
"western_european",
|
||||
"french_european",
|
||||
"germanic_european",
|
||||
"nordic_european",
|
||||
"celtic_european",
|
||||
"slavic_european",
|
||||
"baltic_european",
|
||||
"alpine_european",
|
||||
"balkan_european",
|
||||
"greek_mediterranean",
|
||||
"italian_mediterranean",
|
||||
"iberian_mediterranean",
|
||||
]
|
||||
ETHNICITY_LIST_KEYS = tuple(choice for choice in ETHNICITY_FILTER_CHOICES if choice != "any")
|
||||
ETHNICITY_BASE_LIST_KEYS = (
|
||||
"european",
|
||||
"mediterranean_mena",
|
||||
"latina",
|
||||
"east_asian",
|
||||
"southeast_asian",
|
||||
"south_asian",
|
||||
"black_african",
|
||||
"indigenous",
|
||||
"mixed",
|
||||
)
|
||||
EUROPEAN_REGIONAL_LIST_KEYS = (
|
||||
"western_european",
|
||||
"french_european",
|
||||
"germanic_european",
|
||||
"nordic_european",
|
||||
"celtic_european",
|
||||
"slavic_european",
|
||||
"baltic_european",
|
||||
"alpine_european",
|
||||
"balkan_european",
|
||||
)
|
||||
MEDITERRANEAN_REGIONAL_LIST_KEYS = (
|
||||
"greek_mediterranean",
|
||||
"italian_mediterranean",
|
||||
"iberian_mediterranean",
|
||||
)
|
||||
ETHNICITY_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
|
||||
|
||||
|
||||
def ethnicity_text_from_value(value: Any) -> str:
|
||||
if isinstance(value, dict):
|
||||
return str(value.get("ethnicity") or "").strip()
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
if text.startswith("{"):
|
||||
try:
|
||||
raw = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return text
|
||||
if isinstance(raw, dict):
|
||||
return str(raw.get("ethnicity") or "").strip()
|
||||
return text
|
||||
|
||||
|
||||
def is_valid_ethnicity_filter(value: Any) -> bool:
|
||||
text = ethnicity_text_from_value(value)
|
||||
return text == "any" or text in ETHNICITY_FILTER_CHOICES or "+" in text
|
||||
|
||||
|
||||
def normalize_ethnicity_filter(value: Any, default: str = "any", allow_random: bool = False) -> str:
|
||||
text = ethnicity_text_from_value(value)
|
||||
if text.lower() in ETHNICITY_RANDOM_TOKENS:
|
||||
return "random" if allow_random else default
|
||||
return text if is_valid_ethnicity_filter(text) else default
|
||||
|
||||
|
||||
def build_filter_config_json(
|
||||
ethnicity: str = "any",
|
||||
figure: str = "curvy",
|
||||
no_plus_women: bool = False,
|
||||
no_black: bool = False,
|
||||
include_european: bool = True,
|
||||
include_mediterranean_mena: bool = True,
|
||||
include_latina: bool = True,
|
||||
include_east_asian: bool = True,
|
||||
include_southeast_asian: bool = True,
|
||||
include_south_asian: bool = True,
|
||||
include_black_african: bool = True,
|
||||
include_indigenous: bool = True,
|
||||
include_mixed: bool = True,
|
||||
include_plus_size: bool = True,
|
||||
) -> str:
|
||||
include_flags = {
|
||||
"european": include_european,
|
||||
"mediterranean_mena": include_mediterranean_mena,
|
||||
"latina": include_latina,
|
||||
"east_asian": include_east_asian,
|
||||
"southeast_asian": include_southeast_asian,
|
||||
"south_asian": include_south_asian,
|
||||
"black_african": include_black_african,
|
||||
"indigenous": include_indigenous,
|
||||
"mixed": include_mixed,
|
||||
}
|
||||
selected_ethnicities = [key for key, enabled in include_flags.items() if enabled]
|
||||
disabled_ethnicities = [key for key, enabled in include_flags.items() if not enabled]
|
||||
enabled_ethnicities = list(selected_ethnicities)
|
||||
if enabled_ethnicities:
|
||||
enabled_ethnicities.extend(f"exclude_{key}" for key in disabled_ethnicities)
|
||||
if 0 < len(selected_ethnicities) < len(include_flags):
|
||||
ethnicity = "+".join(enabled_ethnicities)
|
||||
elif not is_valid_ethnicity_filter(ethnicity):
|
||||
ethnicity = "any"
|
||||
return json.dumps(
|
||||
{
|
||||
"ethnicity": ethnicity,
|
||||
"ethnicity_includes": selected_ethnicities,
|
||||
"figure": figure if figure in ("curvy", "balanced", "bombshell", "random") else "curvy",
|
||||
"include_plus_size": bool(include_plus_size),
|
||||
"include_black_african": bool(include_black_african),
|
||||
"no_plus_women": not bool(include_plus_size) or bool(no_plus_women),
|
||||
"no_black": not bool(include_black_african) or bool(no_black),
|
||||
},
|
||||
ensure_ascii=True,
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
|
||||
def build_ethnicity_list_json(
|
||||
include_european: bool = False,
|
||||
include_mediterranean_mena: bool = False,
|
||||
include_latina: bool = False,
|
||||
include_east_asian: bool = False,
|
||||
include_southeast_asian: bool = False,
|
||||
include_south_asian: bool = False,
|
||||
include_black_african: bool = False,
|
||||
include_indigenous: bool = False,
|
||||
include_mixed: bool = False,
|
||||
include_asian: bool = False,
|
||||
include_white_asian: bool = False,
|
||||
include_western_european: bool = False,
|
||||
include_french_european: bool = False,
|
||||
include_germanic_european: bool = False,
|
||||
include_nordic_european: bool = False,
|
||||
include_celtic_european: bool = False,
|
||||
include_slavic_european: bool = False,
|
||||
include_baltic_european: bool = False,
|
||||
include_alpine_european: bool = False,
|
||||
include_balkan_european: bool = False,
|
||||
include_greek_mediterranean: bool = False,
|
||||
include_italian_mediterranean: bool = False,
|
||||
include_iberian_mediterranean: bool = False,
|
||||
strict_excludes: bool = True,
|
||||
) -> dict[str, str]:
|
||||
include_flags = {
|
||||
"european": include_european,
|
||||
"mediterranean_mena": include_mediterranean_mena,
|
||||
"latina": include_latina,
|
||||
"east_asian": include_east_asian,
|
||||
"southeast_asian": include_southeast_asian,
|
||||
"south_asian": include_south_asian,
|
||||
"black_african": include_black_african,
|
||||
"indigenous": include_indigenous,
|
||||
"mixed": include_mixed,
|
||||
"asian": include_asian,
|
||||
"white_asian": include_white_asian,
|
||||
"western_european": include_western_european,
|
||||
"french_european": include_french_european,
|
||||
"germanic_european": include_germanic_european,
|
||||
"nordic_european": include_nordic_european,
|
||||
"celtic_european": include_celtic_european,
|
||||
"slavic_european": include_slavic_european,
|
||||
"baltic_european": include_baltic_european,
|
||||
"alpine_european": include_alpine_european,
|
||||
"balkan_european": include_balkan_european,
|
||||
"greek_mediterranean": include_greek_mediterranean,
|
||||
"italian_mediterranean": include_italian_mediterranean,
|
||||
"iberian_mediterranean": include_iberian_mediterranean,
|
||||
}
|
||||
selected = [key for key in ETHNICITY_LIST_KEYS if include_flags.get(key)]
|
||||
if not selected or set(selected) == set(ETHNICITY_LIST_KEYS):
|
||||
ethnicity = "any"
|
||||
else:
|
||||
tokens = list(selected)
|
||||
if strict_excludes:
|
||||
protected: set[str] = set()
|
||||
if "asian" in selected:
|
||||
protected.update(("east_asian", "southeast_asian", "south_asian"))
|
||||
if "white_asian" in selected:
|
||||
protected.update(("european", "east_asian", "southeast_asian", "south_asian", "mixed"))
|
||||
if any(key in selected for key in EUROPEAN_REGIONAL_LIST_KEYS):
|
||||
protected.add("european")
|
||||
if any(key in selected for key in MEDITERRANEAN_REGIONAL_LIST_KEYS):
|
||||
protected.add("mediterranean_mena")
|
||||
if "mixed" in selected:
|
||||
protected.update(ETHNICITY_BASE_LIST_KEYS)
|
||||
tokens.extend(
|
||||
f"exclude_{key}"
|
||||
for key in ETHNICITY_BASE_LIST_KEYS
|
||||
if key not in selected and key not in protected
|
||||
)
|
||||
ethnicity = "+".join(tokens)
|
||||
filter_config = {
|
||||
"ethnicity": ethnicity,
|
||||
"ethnicity_includes": selected,
|
||||
}
|
||||
summary = "any ethnicity" if ethnicity == "any" else "ethnicity list: " + ", ".join(selected)
|
||||
return {
|
||||
"ethnicity": ethnicity,
|
||||
"filter_config": json.dumps(filter_config, ensure_ascii=True, sort_keys=True),
|
||||
"summary": summary,
|
||||
}
|
||||
|
||||
|
||||
def parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
defaults = {
|
||||
"ethnicity": "any",
|
||||
"figure": "curvy",
|
||||
"no_plus_women": False,
|
||||
"no_black": False,
|
||||
"include_plus_size": True,
|
||||
"include_black_african": True,
|
||||
}
|
||||
if not filter_config:
|
||||
return defaults
|
||||
if isinstance(filter_config, dict):
|
||||
raw = filter_config
|
||||
else:
|
||||
text = str(filter_config).strip()
|
||||
if not text.startswith("{"):
|
||||
raw = {"ethnicity": text}
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(text)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid filter_config JSON: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("filter_config must be a JSON object")
|
||||
parsed = {**defaults, **raw}
|
||||
parsed["ethnicity"] = normalize_ethnicity_filter(parsed.get("ethnicity"), "any")
|
||||
parsed["figure"] = parsed["figure"] if parsed.get("figure") in ("curvy", "balanced", "bombshell", "random") else "curvy"
|
||||
parsed["include_plus_size"] = bool(parsed.get("include_plus_size"))
|
||||
parsed["include_black_african"] = bool(parsed.get("include_black_african"))
|
||||
parsed["no_plus_women"] = bool(parsed.get("no_plus_women"))
|
||||
parsed["no_black"] = bool(parsed.get("no_black"))
|
||||
return parsed
|
||||
|
||||
|
||||
_ethnicity_text_from_value = ethnicity_text_from_value
|
||||
_is_valid_ethnicity_filter = is_valid_ethnicity_filter
|
||||
_parse_filter_config = parse_filter_config
|
||||
@@ -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"
|
||||
@@ -0,0 +1,224 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
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 = (
|
||||
"Ages",
|
||||
"Body types",
|
||||
"Cast",
|
||||
"Cast descriptors",
|
||||
"Characters",
|
||||
"Softcore setup",
|
||||
"Hardcore setup",
|
||||
"POV participant",
|
||||
"Body exposure",
|
||||
"Scene",
|
||||
"Setting",
|
||||
"Pose",
|
||||
"Sexual pose",
|
||||
"Sexual scene",
|
||||
"Facial expression",
|
||||
"Facial expressions",
|
||||
"Clothing",
|
||||
"Clothing state",
|
||||
"Visual clothing state",
|
||||
"Outfit",
|
||||
"Erotic outfit",
|
||||
"Teaser outfit detail",
|
||||
"Softcore visual reference",
|
||||
"Visible remaining styling",
|
||||
"Prop/detail",
|
||||
"Composition",
|
||||
"Role graph",
|
||||
"Camera",
|
||||
"Camera control",
|
||||
"Use",
|
||||
"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, ...]:
|
||||
return DEFAULT_PROMPT_FIELD_LABELS
|
||||
|
||||
|
||||
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 maybe_json(text: Any) -> dict[str, Any] | None:
|
||||
text = clean_text(text)
|
||||
if not text.startswith("{"):
|
||||
return None
|
||||
try:
|
||||
value = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return 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(
|
||||
source_text: str,
|
||||
metadata_json: str,
|
||||
input_hint: str,
|
||||
*,
|
||||
metadata_methods: tuple[str, ...] = METADATA_INPUT_HINTS,
|
||||
text_hint: str = INPUT_HINT_PROMPT,
|
||||
) -> tuple[dict[str, Any] | None, str]:
|
||||
input_hint = normalize_input_hint(input_hint, text_hint=text_hint)
|
||||
if input_hint in metadata_methods:
|
||||
for text, method in ((metadata_json, "metadata_json"), (source_text, "source_json")):
|
||||
row = maybe_json(text)
|
||||
if row is not None:
|
||||
return normalize_input_metadata(row), method
|
||||
return None, "text"
|
||||
|
||||
|
||||
def strip_trigger_prefix(
|
||||
text: Any,
|
||||
trigger_candidates: tuple[str, ...] | list[str],
|
||||
*,
|
||||
preserve_trigger: bool = False,
|
||||
remove_exact: bool = False,
|
||||
) -> str:
|
||||
text = clean_text(text)
|
||||
if remove_exact:
|
||||
text = text.strip(" ,")
|
||||
if preserve_trigger:
|
||||
return text
|
||||
for trigger in trigger_candidates:
|
||||
trigger = clean_text(trigger)
|
||||
if not trigger:
|
||||
continue
|
||||
if text.lower().startswith(trigger.lower() + ","):
|
||||
return text[len(trigger) + 1 :].strip(" ,")
|
||||
if text.lower().startswith(trigger.lower() + "."):
|
||||
return text[len(trigger) + 1 :].strip(" ,")
|
||||
if remove_exact and text.lower() == trigger.lower():
|
||||
return ""
|
||||
return text
|
||||
|
||||
|
||||
def split_avoid(text: Any) -> tuple[str, str]:
|
||||
text = clean_text(text)
|
||||
match = re.search(r"\bAvoid:\s*(.*)$", text)
|
||||
if not match:
|
||||
return text, ""
|
||||
return text[: match.start()].strip(" ."), match.group(1).strip(" .")
|
||||
|
||||
|
||||
def strip_prompt_field_labels(
|
||||
text: Any,
|
||||
*,
|
||||
field_labels: tuple[str, ...] | list[str] = DEFAULT_PROMPT_FIELD_LABELS,
|
||||
) -> str:
|
||||
text = clean_text(text)
|
||||
if not text:
|
||||
return ""
|
||||
labels = "|".join(re.escape(name) for name in sorted(field_labels, key=len, reverse=True))
|
||||
return clean_text(re.sub(rf"\b(?:{labels}):\s*", "", text))
|
||||
|
||||
|
||||
def prompt_field(
|
||||
text: Any,
|
||||
label: str,
|
||||
*,
|
||||
field_labels: tuple[str, ...] | list[str] = DEFAULT_PROMPT_FIELD_LABELS,
|
||||
) -> str:
|
||||
text = clean_text(text)
|
||||
if not text:
|
||||
return ""
|
||||
labels = "|".join(re.escape(name) for name in field_labels)
|
||||
pattern = rf"{re.escape(label)}:\s*(.*?)(?=\. (?:{labels}):|\. Use\b|\. Avoid\b|$)"
|
||||
match = re.search(pattern, text)
|
||||
if not match:
|
||||
return ""
|
||||
return clean_text(match.group(1)).rstrip(".")
|
||||
|
||||
|
||||
def row_value(
|
||||
row: dict[str, Any],
|
||||
key: str,
|
||||
labels: tuple[str, ...] = (),
|
||||
*,
|
||||
field_labels: tuple[str, ...] | list[str] = DEFAULT_PROMPT_FIELD_LABELS,
|
||||
) -> str:
|
||||
value = clean_text(row.get(key, ""))
|
||||
if value:
|
||||
return value
|
||||
prompt = clean_text(row.get("prompt", ""))
|
||||
for label in labels:
|
||||
value = prompt_field(prompt, label, field_labels=field_labels)
|
||||
if value:
|
||||
return value
|
||||
return ""
|
||||
@@ -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"),
|
||||
)
|
||||
+15
-13
@@ -3059,16 +3059,17 @@ def row_base(index: int, batch: int, subject: str, age: str, body: str, scene_sl
|
||||
}
|
||||
|
||||
|
||||
def make_single(index: int, batch: int, rng: random.Random, gender: str, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", poses: str = "standard", backside_bias: float = 0.0, figure: str = "curvy", no_plus: bool = False, no_black: bool = False) -> dict:
|
||||
def make_single(index: int, batch: int, rng: random.Random, gender: str, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", poses: str = "standard", backside_bias: float = 0.0, figure: str = "curvy", no_plus: bool = False, no_black: bool = False, clothing_rng: random.Random | None = None) -> dict:
|
||||
minimal = clothing == "minimal"
|
||||
wardrobe_rng = clothing_rng or rng
|
||||
if gender == "woman":
|
||||
subject, age, body, skin, hair, eyes = choose_woman(rng, ethnicity, no_plus, no_black)
|
||||
clothes = choose(rng, WOMEN_CLOTHES_MINIMAL if minimal else WOMEN_CLOTHES)
|
||||
clothes = choose(wardrobe_rng, WOMEN_CLOTHES_MINIMAL if minimal else WOMEN_CLOTHES)
|
||||
figure_note = choose(rng, figure_pool(figure))
|
||||
else:
|
||||
men_pool = by_ethnicity(MEN, ethnicity)
|
||||
subject, age, body, skin, hair, eyes = choose(rng, men_pool)
|
||||
clothes = choose(rng, MEN_CLOTHES_MINIMAL if minimal else MEN_CLOTHES)
|
||||
clothes = choose(wardrobe_rng, MEN_CLOTHES_MINIMAL if minimal else MEN_CLOTHES)
|
||||
figure_note = ""
|
||||
body_phrase = make_body_phrase(body, figure_note)
|
||||
|
||||
@@ -3119,7 +3120,7 @@ def make_single(index: int, batch: int, rng: random.Random, gender: str, expr_de
|
||||
return row
|
||||
|
||||
|
||||
def make_couple(index: int, batch: int, rng: random.Random, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", no_plus: bool = False) -> dict:
|
||||
def make_couple(index: int, batch: int, rng: random.Random, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", no_plus: bool = False, clothing_rng: random.Random | None = None) -> dict:
|
||||
primary_subject, subject_phrase, pose = choose(rng, COUPLE_TYPES)
|
||||
if ethnicity == "asian":
|
||||
subject_phrase = {
|
||||
@@ -3140,7 +3141,7 @@ def make_couple(index: int, batch: int, rng: random.Random, expr_deck: Expressio
|
||||
if no_plus:
|
||||
body_options = [b for b in body_options if "plus" not in b and "fat" not in b]
|
||||
body = choose(rng, body_options)
|
||||
outfits = choose(rng, COUPLE_OUTFITS_MINIMAL if clothing == "minimal" else COUPLE_OUTFITS)
|
||||
outfits = choose(clothing_rng or rng, COUPLE_OUTFITS_MINIMAL if clothing == "minimal" else COUPLE_OUTFITS)
|
||||
expr_a, expr_b = expr_deck.draw_two()
|
||||
|
||||
row = row_base(index, batch, primary_subject, ages, body, scene_slug, composition)
|
||||
@@ -3164,7 +3165,7 @@ def make_couple(index: int, batch: int, rng: random.Random, expr_deck: Expressio
|
||||
return row
|
||||
|
||||
|
||||
def make_group_or_layout(index: int, batch: int, rng: random.Random, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", no_plus: bool = False) -> dict:
|
||||
def make_group_or_layout(index: int, batch: int, rng: random.Random, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", no_plus: bool = False, clothing_rng: random.Random | None = None) -> dict:
|
||||
minimal = clothing == "minimal"
|
||||
group_outfits = "minimal beachwear and lingerie-inspired outfits" if minimal else "stylish revealing party outfits"
|
||||
if ethnicity == "asian":
|
||||
@@ -3203,7 +3204,7 @@ def make_group_or_layout(index: int, batch: int, rng: random.Random, expr_deck:
|
||||
)
|
||||
return row
|
||||
|
||||
layout_slug, layout_desc = choose(rng, LAYOUTS_MINIMAL if minimal else LAYOUTS_FULL)
|
||||
layout_slug, layout_desc = choose(clothing_rng or rng, LAYOUTS_MINIMAL if minimal else LAYOUTS_FULL)
|
||||
if ethnicity == "asian":
|
||||
layout_desc = layout_desc.replace("adult", "Asian adult")
|
||||
elif ethnicity == "white_asian":
|
||||
@@ -3231,8 +3232,9 @@ def make_group_or_layout(index: int, batch: int, rng: random.Random, expr_deck:
|
||||
return row
|
||||
|
||||
|
||||
def build_rows(total: int, start_index: int, clothing: str = "full", ethnicity: str = "any", poses: str = "standard", backside_bias: float = 0.0, figure: str = "curvy", no_plus: bool = False, no_black: bool = False, minimal_clothing_ratio: float | None = None, standard_pose_ratio: float | None = None, seed: int = DEFAULT_RNG_SEED, expression_seed: int = EXPRESSION_SEED) -> list[dict]:
|
||||
def build_rows(total: int, start_index: int, clothing: str = "full", ethnicity: str = "any", poses: str = "standard", backside_bias: float = 0.0, figure: str = "curvy", no_plus: bool = False, no_black: bool = False, minimal_clothing_ratio: float | None = None, standard_pose_ratio: float | None = None, seed: int = DEFAULT_RNG_SEED, expression_seed: int = EXPRESSION_SEED, clothing_rng: random.Random | None = None) -> list[dict]:
|
||||
rng = random.Random(seed)
|
||||
wardrobe_rng = clothing_rng or rng
|
||||
expr_deck = ExpressionDeck(EXPRESSIONS, random.Random(expression_seed))
|
||||
rows: list[dict] = []
|
||||
batch_quotas = batch_category_quotas()
|
||||
@@ -3244,21 +3246,21 @@ def build_rows(total: int, start_index: int, clothing: str = "full", ethnicity:
|
||||
index = start_index
|
||||
for batch in range(1, batch_count + 1):
|
||||
batch_rows: list[dict] = []
|
||||
clothing_modes = batch_clothing_modes(rng, clothing, minimal_clothing_ratio)
|
||||
clothing_modes = batch_clothing_modes(wardrobe_rng, clothing, minimal_clothing_ratio)
|
||||
single_pose_modes = batch_single_pose_modes(rng, poses, standard_pose_ratio, single_subject_count)
|
||||
for category, count in batch_quotas:
|
||||
for _ in range(count):
|
||||
row_clothing = clothing_modes.pop()
|
||||
if category == "woman":
|
||||
row_pose = single_pose_modes.pop()
|
||||
row = make_single(index, batch, rng, "woman", expr_deck, row_clothing, ethnicity, row_pose, backside_bias, figure, no_plus, no_black)
|
||||
row = make_single(index, batch, rng, "woman", expr_deck, row_clothing, ethnicity, row_pose, backside_bias, figure, no_plus, no_black, clothing_rng=wardrobe_rng if clothing_rng else None)
|
||||
elif category == "man":
|
||||
row_pose = single_pose_modes.pop()
|
||||
row = make_single(index, batch, rng, "man", expr_deck, row_clothing, ethnicity, row_pose, backside_bias, figure, no_plus, no_black)
|
||||
row = make_single(index, batch, rng, "man", expr_deck, row_clothing, ethnicity, row_pose, backside_bias, figure, no_plus, no_black, clothing_rng=wardrobe_rng if clothing_rng else None)
|
||||
elif category == "couple":
|
||||
row = make_couple(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus)
|
||||
row = make_couple(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus, clothing_rng=wardrobe_rng if clothing_rng else None)
|
||||
else:
|
||||
row = make_group_or_layout(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus)
|
||||
row = make_group_or_layout(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus, clothing_rng=wardrobe_rng if clothing_rng else None)
|
||||
batch_rows.append(row)
|
||||
index += 1
|
||||
rng.shuffle(batch_rows)
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
GENERATION_PROFILE_PRESETS = {
|
||||
"balanced": {
|
||||
"clothing": "full",
|
||||
"poses": "standard",
|
||||
"expression_enabled": True,
|
||||
"expression_intensity": 0.5,
|
||||
"backside_bias": 0.0,
|
||||
"minimal_clothing_ratio": -1.0,
|
||||
"standard_pose_ratio": -1.0,
|
||||
"trigger": "sxcpinup_coloredpencil",
|
||||
"prepend_trigger_to_prompt": True,
|
||||
},
|
||||
"casual_clean": {
|
||||
"clothing": "full",
|
||||
"poses": "standard",
|
||||
"expression_enabled": True,
|
||||
"expression_intensity": 0.35,
|
||||
"backside_bias": 0.0,
|
||||
"minimal_clothing_ratio": -1.0,
|
||||
"standard_pose_ratio": -1.0,
|
||||
"trigger": "sxcpinup_coloredpencil",
|
||||
"prepend_trigger_to_prompt": True,
|
||||
},
|
||||
"evocative_softcore": {
|
||||
"clothing": "minimal",
|
||||
"poses": "evocative",
|
||||
"expression_enabled": True,
|
||||
"expression_intensity": 0.65,
|
||||
"backside_bias": 0.2,
|
||||
"minimal_clothing_ratio": -1.0,
|
||||
"standard_pose_ratio": -1.0,
|
||||
"trigger": "sxcpinup_coloredpencil",
|
||||
"prepend_trigger_to_prompt": True,
|
||||
},
|
||||
"hardcore_intense": {
|
||||
"clothing": "minimal",
|
||||
"poses": "evocative",
|
||||
"expression_enabled": True,
|
||||
"expression_intensity": 0.9,
|
||||
"backside_bias": 0.0,
|
||||
"minimal_clothing_ratio": -1.0,
|
||||
"standard_pose_ratio": -1.0,
|
||||
"trigger": "sxcpinup_coloredpencil",
|
||||
"prepend_trigger_to_prompt": True,
|
||||
},
|
||||
"krea2_friendly": {
|
||||
"clothing": "full",
|
||||
"poses": "standard",
|
||||
"expression_enabled": True,
|
||||
"expression_intensity": 0.55,
|
||||
"backside_bias": 0.0,
|
||||
"minimal_clothing_ratio": -1.0,
|
||||
"standard_pose_ratio": -1.0,
|
||||
"trigger": "sxcpinup_coloredpencil",
|
||||
"prepend_trigger_to_prompt": False,
|
||||
},
|
||||
"flux_original": {
|
||||
"clothing": "full",
|
||||
"poses": "standard",
|
||||
"expression_enabled": True,
|
||||
"expression_intensity": 0.5,
|
||||
"backside_bias": 0.0,
|
||||
"minimal_clothing_ratio": -1.0,
|
||||
"standard_pose_ratio": -1.0,
|
||||
"trigger": "sxcpinup_coloredpencil",
|
||||
"prepend_trigger_to_prompt": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _is_false(value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value is False
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ("false", "0", "no", "off")
|
||||
return False
|
||||
|
||||
|
||||
def _clamped_float(value: Any, default: float = 0.5, min_value: float = 0.0, max_value: float = 1.0) -> float:
|
||||
try:
|
||||
number = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return max(min_value, min(max_value, number))
|
||||
|
||||
|
||||
def generation_profile_choices() -> list[str]:
|
||||
return list(GENERATION_PROFILE_PRESETS)
|
||||
|
||||
|
||||
def build_generation_profile_json(
|
||||
profile: str = "balanced",
|
||||
clothing_override: str = "profile_default",
|
||||
poses_override: str = "profile_default",
|
||||
expression_intensity_mode: str = "profile_default",
|
||||
expression_intensity: float = -1.0,
|
||||
backside_bias: float = -1.0,
|
||||
minimal_clothing_ratio: float = -1.0,
|
||||
standard_pose_ratio: float = -1.0,
|
||||
trigger_policy: str = "profile_default",
|
||||
expression_enabled: bool = True,
|
||||
) -> str:
|
||||
profile = profile if profile in GENERATION_PROFILE_PRESETS else "balanced"
|
||||
config = dict(GENERATION_PROFILE_PRESETS[profile])
|
||||
if clothing_override in ("full", "minimal", "random"):
|
||||
config["clothing"] = clothing_override
|
||||
if poses_override in ("standard", "evocative", "random"):
|
||||
config["poses"] = poses_override
|
||||
config["expression_enabled"] = not _is_false(expression_enabled)
|
||||
if expression_intensity_mode == "random":
|
||||
config["expression_intensity"] = -1.0
|
||||
elif expression_intensity_mode == "fixed" and float(expression_intensity) >= 0:
|
||||
config["expression_intensity"] = _clamped_float(expression_intensity, config["expression_intensity"])
|
||||
if float(backside_bias) >= 0:
|
||||
config["backside_bias"] = _clamped_float(backside_bias, config["backside_bias"])
|
||||
if float(minimal_clothing_ratio) >= 0:
|
||||
config["minimal_clothing_ratio"] = _clamped_float(minimal_clothing_ratio, config["minimal_clothing_ratio"])
|
||||
if float(standard_pose_ratio) >= 0:
|
||||
config["standard_pose_ratio"] = _clamped_float(standard_pose_ratio, config["standard_pose_ratio"])
|
||||
if trigger_policy == "prepend_trigger":
|
||||
config["prepend_trigger_to_prompt"] = True
|
||||
elif trigger_policy == "do_not_prepend":
|
||||
config["prepend_trigger_to_prompt"] = False
|
||||
config["profile"] = profile
|
||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def parse_generation_profile(profile_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not profile_config:
|
||||
return dict(GENERATION_PROFILE_PRESETS["balanced"])
|
||||
if isinstance(profile_config, dict):
|
||||
raw = profile_config
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(profile_config))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid generation_profile JSON: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("generation_profile must be a JSON object")
|
||||
profile = str(raw.get("profile") or "balanced")
|
||||
parsed = dict(GENERATION_PROFILE_PRESETS.get(profile, GENERATION_PROFILE_PRESETS["balanced"]))
|
||||
parsed.update(raw)
|
||||
parsed["clothing"] = parsed["clothing"] if parsed.get("clothing") in ("full", "minimal", "random") else "full"
|
||||
parsed["poses"] = parsed["poses"] if parsed.get("poses") in ("standard", "evocative", "random") else "standard"
|
||||
parsed["expression_enabled"] = not _is_false(parsed.get("expression_enabled", True))
|
||||
try:
|
||||
raw_expression_intensity = float(parsed.get("expression_intensity"))
|
||||
except (TypeError, ValueError):
|
||||
raw_expression_intensity = 0.5
|
||||
parsed["expression_intensity"] = -1.0 if raw_expression_intensity < 0 else _clamped_float(raw_expression_intensity, 0.5)
|
||||
parsed["backside_bias"] = _clamped_float(parsed.get("backside_bias"), 0.0)
|
||||
parsed["minimal_clothing_ratio"] = _clamped_float(parsed.get("minimal_clothing_ratio"), -1.0, -1.0, 1.0)
|
||||
parsed["standard_pose_ratio"] = _clamped_float(parsed.get("standard_pose_ratio"), -1.0, -1.0, 1.0)
|
||||
parsed["trigger"] = str(parsed.get("trigger") or "sxcpinup_coloredpencil")
|
||||
parsed["prepend_trigger_to_prompt"] = bool(parsed.get("prepend_trigger_to_prompt"))
|
||||
return parsed
|
||||
|
||||
|
||||
_parse_generation_profile = parse_generation_profile
|
||||
@@ -0,0 +1,164 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from .krea_action_context import (
|
||||
axis_values_text,
|
||||
is_climax_text,
|
||||
is_foreplay_text,
|
||||
is_oral_text,
|
||||
is_outercourse_text,
|
||||
is_toy_assisted_double_text,
|
||||
is_vaginal_penetration_text,
|
||||
)
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
from krea_action_context import (
|
||||
axis_values_text,
|
||||
is_climax_text,
|
||||
is_foreplay_text,
|
||||
is_oral_text,
|
||||
is_outercourse_text,
|
||||
is_toy_assisted_double_text,
|
||||
is_vaginal_penetration_text,
|
||||
)
|
||||
|
||||
|
||||
ACTION_CLIMAX = "climax"
|
||||
ACTION_ANAL = "anal"
|
||||
ACTION_FOREPLAY = "foreplay"
|
||||
ACTION_MANUAL = "manual"
|
||||
ACTION_OUTERCOURSE = "outercourse"
|
||||
ACTION_ORAL = "oral"
|
||||
ACTION_PENETRATION = "penetration"
|
||||
ACTION_THREESOME = "threesome"
|
||||
ACTION_GROUP = "group"
|
||||
ACTION_TOY_DOUBLE = "toy_double"
|
||||
ACTION_DEFAULT = "default"
|
||||
|
||||
HARDCORE_ACTION_FAMILY_CHOICES = {
|
||||
ACTION_CLIMAX,
|
||||
ACTION_ANAL,
|
||||
ACTION_FOREPLAY,
|
||||
ACTION_MANUAL,
|
||||
ACTION_OUTERCOURSE,
|
||||
ACTION_ORAL,
|
||||
ACTION_PENETRATION,
|
||||
ACTION_THREESOME,
|
||||
ACTION_GROUP,
|
||||
ACTION_TOY_DOUBLE,
|
||||
ACTION_DEFAULT,
|
||||
}
|
||||
|
||||
|
||||
def normalize_hardcore_action_family(value: Any, default: str = "") -> str:
|
||||
text = re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().lower()).strip("_")
|
||||
aliases = {
|
||||
"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
|
||||
|
||||
|
||||
def infer_hardcore_action_family(
|
||||
role_graph: str,
|
||||
hard_item: str,
|
||||
composition: str = "",
|
||||
axis_values: Any = None,
|
||||
*,
|
||||
is_climax: bool | None = None,
|
||||
) -> str:
|
||||
axis_text = axis_values_text(axis_values)
|
||||
if is_climax is None:
|
||||
is_climax = is_climax_text(role_graph, hard_item, composition, axis_text)
|
||||
if is_climax:
|
||||
return ACTION_CLIMAX
|
||||
if is_foreplay_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_FOREPLAY
|
||||
if is_outercourse_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_OUTERCOURSE
|
||||
if is_oral_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_ORAL
|
||||
if is_vaginal_penetration_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_PENETRATION
|
||||
if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_TOY_DOUBLE
|
||||
return ACTION_DEFAULT
|
||||
|
||||
|
||||
def source_hardcore_action_family(
|
||||
source_family: Any,
|
||||
role_graph: str,
|
||||
hard_item: str,
|
||||
composition: str = "",
|
||||
axis_values: Any = None,
|
||||
) -> str:
|
||||
inferred = infer_hardcore_action_family(role_graph, hard_item, composition, axis_values)
|
||||
if inferred in (ACTION_CLIMAX, ACTION_TOY_DOUBLE):
|
||||
return inferred
|
||||
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 = {
|
||||
"penetrative": ACTION_PENETRATION,
|
||||
"anal": ACTION_ANAL,
|
||||
"foreplay": ACTION_FOREPLAY,
|
||||
"interaction": ACTION_FOREPLAY,
|
||||
"manual": ACTION_MANUAL,
|
||||
"oral": ACTION_ORAL,
|
||||
"outercourse": ACTION_OUTERCOURSE,
|
||||
"threesome": ACTION_THREESOME,
|
||||
"group": ACTION_GROUP,
|
||||
"climax": ACTION_CLIMAX,
|
||||
}
|
||||
return source_mapping.get(family, inferred)
|
||||
@@ -0,0 +1,960 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from string import Formatter
|
||||
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 = [
|
||||
"any",
|
||||
"penetrative",
|
||||
"foreplay",
|
||||
"interaction",
|
||||
"manual",
|
||||
"oral",
|
||||
"outercourse",
|
||||
"anal",
|
||||
"climax",
|
||||
"threesome",
|
||||
"group",
|
||||
]
|
||||
HARDCORE_POSITION_FOCUS_CHOICES = [
|
||||
"keep_pool",
|
||||
"penetration_only",
|
||||
"foreplay_only",
|
||||
"interaction_only",
|
||||
"manual_only",
|
||||
"oral_only",
|
||||
"outercourse_only",
|
||||
"anal_only",
|
||||
"climax_only",
|
||||
"threesome_only",
|
||||
"group_only",
|
||||
]
|
||||
HARDCORE_POSITION_KEY_CHOICES = [
|
||||
"missionary",
|
||||
"missionary_folded",
|
||||
"cowgirl",
|
||||
"cowgirl_alt",
|
||||
"reverse_cowgirl",
|
||||
"reverse_cowgirl_alt",
|
||||
"doggy",
|
||||
"bent_over",
|
||||
"face_down_ass_up",
|
||||
"standing",
|
||||
"side_lying",
|
||||
"edge_supported",
|
||||
"kneeling",
|
||||
"top_down_oral",
|
||||
"lotus_lap",
|
||||
"face_sitting",
|
||||
"sixty_nine",
|
||||
"reclining_oral",
|
||||
"blowjob_sitting",
|
||||
"straddled_oral",
|
||||
"spread_leg_oral",
|
||||
"chair_oral",
|
||||
"kissing",
|
||||
"caressing",
|
||||
"breast_touch",
|
||||
"face_touch",
|
||||
"undressing",
|
||||
"body_worship",
|
||||
"nipple_play",
|
||||
"ass_grab",
|
||||
"thigh_kissing",
|
||||
"hair_holding",
|
||||
"wrist_pinning",
|
||||
"dirty_talk",
|
||||
"position_transition",
|
||||
"guided_positioning",
|
||||
"camera_showing",
|
||||
"watching",
|
||||
"aftercare",
|
||||
"cleanup",
|
||||
"fingering",
|
||||
"clit_rubbing",
|
||||
"mutual_masturbation",
|
||||
"boobjob",
|
||||
"testicle_sucking",
|
||||
"penis_licking",
|
||||
"handjob",
|
||||
"footjob",
|
||||
"open_thighs",
|
||||
"front_back",
|
||||
]
|
||||
HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
|
||||
"any": [
|
||||
"penetrative_sex",
|
||||
"foreplay_teasing",
|
||||
"body_worship_touching",
|
||||
"clothing_position_transitions",
|
||||
"dominant_guidance",
|
||||
"camera_performance",
|
||||
"manual_stimulation",
|
||||
"oral_sex",
|
||||
"outercourse_sex",
|
||||
"anal_double_penetration",
|
||||
"threesomes",
|
||||
"group_coordination",
|
||||
"group_sex_orgy",
|
||||
"cumshot_climax",
|
||||
"aftercare_cleanup",
|
||||
],
|
||||
"penetrative": ["penetrative_sex"],
|
||||
"foreplay": ["foreplay_teasing"],
|
||||
"interaction": [
|
||||
"foreplay_teasing",
|
||||
"body_worship_touching",
|
||||
"clothing_position_transitions",
|
||||
"dominant_guidance",
|
||||
"camera_performance",
|
||||
"group_coordination",
|
||||
"aftercare_cleanup",
|
||||
],
|
||||
"manual": ["manual_stimulation"],
|
||||
"oral": ["oral_sex"],
|
||||
"outercourse": ["outercourse_sex", "manual_stimulation"],
|
||||
"anal": ["anal_double_penetration"],
|
||||
"climax": ["cumshot_climax"],
|
||||
"threesome": ["threesomes"],
|
||||
"group": ["group_sex_orgy"],
|
||||
}
|
||||
HARDCORE_POSITION_KEY_MATCHES = {
|
||||
"missionary": ("missionary", "above her", "under her"),
|
||||
"missionary_folded": ("folded missionary", "knees-to-chest", "knees to chest", "folded legs", "folded high"),
|
||||
"cowgirl": ("cowgirl", "straddling", "straddles", "on top", "squatting on top"),
|
||||
"cowgirl_alt": ("cowgirl-alt", "low cowgirl", "seated-squat cowgirl", "low seated squat"),
|
||||
"reverse_cowgirl": ("reverse cowgirl", "facing away"),
|
||||
"reverse_cowgirl_alt": ("reverse cowgirl alt", "upright reverse cowgirl", "upright back-facing straddle"),
|
||||
"doggy": ("doggy", "all fours", "rear-entry", "from behind"),
|
||||
"bent_over": ("bent-over", "bent over", "hips raised"),
|
||||
"face_down_ass_up": ("face-down", "ass-up"),
|
||||
"standing": ("standing", "stands", "braced standing"),
|
||||
"side_lying": ("side-lying", "side lying", "spooning", "on the side", "on her side"),
|
||||
"edge_supported": ("edge-of-bed", "edge of bed", "bed edge", "raised edge", "edge-supported"),
|
||||
"kneeling": ("kneeling", "kneels", "kneeling center"),
|
||||
"top_down_oral": ("top-down oral", "top view oral", "top-view oral", "nadir-angle", "overhead oral"),
|
||||
"lotus_lap": ("lotus", "lap", "seated in a partner's lap"),
|
||||
"face_sitting": ("face-sitting", "face sitting"),
|
||||
"sixty_nine": ("sixty-nine", "69"),
|
||||
"reclining_oral": ("reclining cunnilingus",),
|
||||
"blowjob_sitting": ("blowjob_sitting", "upright sitting oral", "sitting upright oral", "seated oral"),
|
||||
"straddled_oral": ("straddled oral",),
|
||||
"spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"),
|
||||
"chair_oral": ("chair oral",),
|
||||
"kissing": ("kiss", "kissing", "mouth-to-mouth", "mouth to mouth", "lips pressed"),
|
||||
"caressing": ("caress", "caressing", "hands roaming", "stroking skin", "hands sliding"),
|
||||
"breast_touch": ("breast", "breasts", "nipple", "cupping breasts", "touching breasts"),
|
||||
"face_touch": ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin"),
|
||||
"undressing": ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning"),
|
||||
"body_worship": ("body worship", "worship", "kissing down", "mouth on skin", "kissing the body"),
|
||||
"nipple_play": ("nipple", "nipples", "licking nipples", "sucking nipples", "nipple play"),
|
||||
"ass_grab": ("ass grab", "ass-grab", "ass grabbing", "hand on the ass", "squeezing the ass"),
|
||||
"thigh_kissing": ("thigh kiss", "thigh kissing", "kissing thighs", "mouth on inner thighs"),
|
||||
"hair_holding": ("hair holding", "hair held", "holding hair", "hair pulled back"),
|
||||
"wrist_pinning": ("wrist", "wrists", "pinning wrists", "wrists pinned", "hands pinned"),
|
||||
"dirty_talk": ("dirty talk", "whispering", "mouth near the ear", "telling", "verbal teasing"),
|
||||
"position_transition": ("transition", "turning around", "pulling onto the bed", "moving into position", "position change"),
|
||||
"guided_positioning": ("guiding", "guided", "guides", "lifting legs", "spreading thighs", "pulling hips", "turning the body"),
|
||||
"camera_showing": ("camera", "showing to camera", "presenting to camera", "spread open for camera", "creator-shot"),
|
||||
"watching": ("watching", "voyeur", "waiting turn", "partner watches", "onlooker"),
|
||||
"aftercare": ("aftercare", "cuddling", "kissing after", "holding close", "post-sex"),
|
||||
"cleanup": ("cleanup", "wiping", "cleaning", "towel", "wet cloth"),
|
||||
"fingering": ("fingering", "fingers inside", "fingers in pussy", "finger stimulation"),
|
||||
"clit_rubbing": ("clit", "clitoris", "clit rubbing", "rubbing the clit", "fingers on clit"),
|
||||
"mutual_masturbation": ("mutual masturbation", "both touching themselves", "masturbating together", "hands on their own bodies"),
|
||||
"boobjob": ("boobjob", "titjob", "breast-sex", "breast sex"),
|
||||
"testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"),
|
||||
"penis_licking": ("penis-licking", "penis licking", "tongue along", "tongue licking"),
|
||||
"handjob": ("handjob", "hand job", "stroking the penis", "hand stroking", "manual stimulation"),
|
||||
"footjob": ("footjob", "soles", "toes curled", "feet stroking"),
|
||||
"open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"),
|
||||
"front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"),
|
||||
}
|
||||
|
||||
|
||||
def _text_matches_position_key(text: str, position: str) -> bool:
|
||||
terms = HARDCORE_POSITION_KEY_MATCHES.get(position, ())
|
||||
if not any(term in text for term in terms):
|
||||
return False
|
||||
if position == "missionary" and any(term in text for term in HARDCORE_POSITION_KEY_MATCHES["missionary_folded"]):
|
||||
return False
|
||||
if position == "cowgirl" and any(
|
||||
term in text
|
||||
for term in (
|
||||
HARDCORE_POSITION_KEY_MATCHES["cowgirl_alt"]
|
||||
+ HARDCORE_POSITION_KEY_MATCHES["reverse_cowgirl"]
|
||||
+ HARDCORE_POSITION_KEY_MATCHES["reverse_cowgirl_alt"]
|
||||
)
|
||||
):
|
||||
return False
|
||||
if position == "reverse_cowgirl" and any(term in text for term in HARDCORE_POSITION_KEY_MATCHES["reverse_cowgirl_alt"]):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
HARDCORE_POSITION_AXIS_KEYS = {
|
||||
"position",
|
||||
"body_position",
|
||||
"body_arrangement",
|
||||
"arrangement",
|
||||
"tease_act",
|
||||
"touch_detail",
|
||||
"manual_act",
|
||||
"manual_detail",
|
||||
"worship_act",
|
||||
"transition_act",
|
||||
"control_act",
|
||||
"performance_act",
|
||||
"coordination_act",
|
||||
"aftercare_act",
|
||||
"cleanup_detail",
|
||||
}
|
||||
RESTORE_PROMPT_AXIS_CHOICES = [
|
||||
"clothing_detail",
|
||||
"face_detail",
|
||||
"expression_detail",
|
||||
"mouth_detail",
|
||||
"reaction_detail",
|
||||
"body_contact",
|
||||
"hand_detail",
|
||||
"touch_detail",
|
||||
"foreplay_detail",
|
||||
"performance_act",
|
||||
"visibility",
|
||||
"angle",
|
||||
]
|
||||
HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY = {
|
||||
"penetrative_sex": "penetrative",
|
||||
"foreplay_teasing": "foreplay",
|
||||
"body_worship_touching": "interaction",
|
||||
"clothing_position_transitions": "interaction",
|
||||
"dominant_guidance": "interaction",
|
||||
"camera_performance": "interaction",
|
||||
"manual_stimulation": "manual",
|
||||
"oral_sex": "oral",
|
||||
"outercourse_sex": "outercourse",
|
||||
"anal_double_penetration": "anal",
|
||||
"threesomes": "threesome",
|
||||
"group_coordination": "interaction",
|
||||
"group_sex_orgy": "group",
|
||||
"cumshot_climax": "climax",
|
||||
"aftercare_cleanup": "interaction",
|
||||
}
|
||||
FOCUS_FAMILY_BY_KEY = {
|
||||
"penetration_only": "penetrative",
|
||||
"foreplay_only": "foreplay",
|
||||
"interaction_only": "interaction",
|
||||
"manual_only": "manual",
|
||||
"oral_only": "oral",
|
||||
"outercourse_only": "outercourse",
|
||||
"anal_only": "anal",
|
||||
"climax_only": "climax",
|
||||
"threesome_only": "threesome",
|
||||
"group_only": "group",
|
||||
}
|
||||
|
||||
|
||||
def _is_false(value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value is False
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ("false", "0", "no", "off")
|
||||
return False
|
||||
|
||||
|
||||
def _list_from(value: Any) -> list[Any]:
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return [value]
|
||||
|
||||
|
||||
def normalize_restore_prompt_axes(values: Any) -> list[str]:
|
||||
allowed = set(RESTORE_PROMPT_AXIS_CHOICES)
|
||||
normalized: list[str] = []
|
||||
for value in _list_from(values):
|
||||
text = str(value or "").strip()
|
||||
if text in allowed and text not in normalized:
|
||||
normalized.append(text)
|
||||
return normalized
|
||||
|
||||
|
||||
def _entry_text(item: Any) -> str:
|
||||
if isinstance(item, dict):
|
||||
return str(
|
||||
item.get("template")
|
||||
or item.get("prompt")
|
||||
or item.get("text")
|
||||
or item.get("description")
|
||||
or item.get("name")
|
||||
or ""
|
||||
).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]:
|
||||
return list(HARDCORE_POSITION_FAMILY_CHOICES)
|
||||
|
||||
|
||||
def hardcore_position_focus_choices() -> list[str]:
|
||||
return list(HARDCORE_POSITION_FOCUS_CHOICES)
|
||||
|
||||
|
||||
def hardcore_position_key_choices() -> list[str]:
|
||||
return list(HARDCORE_POSITION_KEY_CHOICES)
|
||||
|
||||
|
||||
def normalize_hardcore_position_family(value: Any, default: str = "any") -> str:
|
||||
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
|
||||
|
||||
|
||||
def normalize_hardcore_position_values(values: Any) -> list[str]:
|
||||
raw_values = _list_from(values)
|
||||
selected: list[str] = []
|
||||
for value in raw_values:
|
||||
text = str(value or "").strip()
|
||||
if not text or text == "any":
|
||||
continue
|
||||
normalized = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
|
||||
if normalized in HARDCORE_POSITION_KEY_CHOICES and normalized not in selected:
|
||||
selected.append(normalized)
|
||||
return selected
|
||||
|
||||
|
||||
def empty_hardcore_position_config() -> dict[str, Any]:
|
||||
return {
|
||||
"config_type": "hardcore_position",
|
||||
"enabled": False,
|
||||
"family": "any",
|
||||
"positions": [],
|
||||
"require_position": False,
|
||||
"allow_toys": True,
|
||||
"allow_double": True,
|
||||
"allow_penetration": True,
|
||||
"allow_foreplay": True,
|
||||
"allow_interaction": True,
|
||||
"allow_manual": True,
|
||||
"allow_oral": True,
|
||||
"allow_outercourse": True,
|
||||
"allow_anal": True,
|
||||
"allow_climax": True,
|
||||
"restore_prompt_axes": [],
|
||||
"relax_non_pose_axis_conflicts": False,
|
||||
}
|
||||
|
||||
|
||||
def parse_hardcore_position_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not value:
|
||||
return empty_hardcore_position_config()
|
||||
if isinstance(value, dict):
|
||||
raw = value
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(value))
|
||||
except json.JSONDecodeError:
|
||||
return empty_hardcore_position_config()
|
||||
if not isinstance(raw, dict):
|
||||
return empty_hardcore_position_config()
|
||||
parsed = {**empty_hardcore_position_config(), **raw}
|
||||
parsed["enabled"] = bool(parsed.get("enabled", True))
|
||||
parsed["family"] = normalize_hardcore_position_family(parsed.get("family"))
|
||||
parsed["positions"] = normalize_hardcore_position_values(parsed.get("positions"))
|
||||
parsed["require_position"] = not _is_false(parsed.get("require_position", False))
|
||||
for key in (
|
||||
"allow_toys",
|
||||
"allow_double",
|
||||
"allow_penetration",
|
||||
"allow_foreplay",
|
||||
"allow_interaction",
|
||||
"allow_manual",
|
||||
"allow_oral",
|
||||
"allow_outercourse",
|
||||
"allow_anal",
|
||||
"allow_climax",
|
||||
):
|
||||
parsed[key] = not _is_false(parsed.get(key, True))
|
||||
parsed["restore_prompt_axes"] = normalize_restore_prompt_axes(parsed.get("restore_prompt_axes"))
|
||||
parsed["relax_non_pose_axis_conflicts"] = not _is_false(parsed.get("relax_non_pose_axis_conflicts", False))
|
||||
return parsed
|
||||
|
||||
|
||||
def hardcore_position_summary(config: dict[str, Any]) -> str:
|
||||
if not config.get("enabled"):
|
||||
return "hardcore position unrestricted"
|
||||
parts = [f"family={config.get('family', 'any')}"]
|
||||
positions = config.get("positions") or []
|
||||
if positions:
|
||||
parts.append("positions=" + ",".join(positions))
|
||||
elif config.get("require_position"):
|
||||
parts.append("position_templates=required")
|
||||
disabled = [
|
||||
label
|
||||
for key, label in (
|
||||
("allow_toys", "toys"),
|
||||
("allow_double", "double"),
|
||||
("allow_penetration", "penetration"),
|
||||
("allow_foreplay", "foreplay"),
|
||||
("allow_interaction", "interaction"),
|
||||
("allow_manual", "manual"),
|
||||
("allow_oral", "oral"),
|
||||
("allow_outercourse", "outercourse"),
|
||||
("allow_anal", "anal"),
|
||||
("allow_climax", "climax"),
|
||||
)
|
||||
if not config.get(key, True)
|
||||
]
|
||||
if disabled:
|
||||
parts.append("blocked=" + ",".join(disabled))
|
||||
restore_axes = normalize_restore_prompt_axes(config.get("restore_prompt_axes"))
|
||||
if restore_axes:
|
||||
parts.append("restore_axes=" + ",".join(restore_axes))
|
||||
if restore_axes and config.get("relax_non_pose_axis_conflicts"):
|
||||
parts.append("relaxed_non_pose_conflicts")
|
||||
return "; ".join(parts)
|
||||
|
||||
|
||||
def build_hardcore_position_pool_json(
|
||||
hardcore_position_config: str | dict[str, Any] | None = "",
|
||||
combine_mode: str = "replace",
|
||||
family: str = "any",
|
||||
selected_positions: list[str] | tuple[str, ...] | str | None = None,
|
||||
) -> str:
|
||||
base = parse_hardcore_position_config(hardcore_position_config)
|
||||
if combine_mode == "replace":
|
||||
restore_axes = normalize_restore_prompt_axes(base.get("restore_prompt_axes"))
|
||||
relax_non_pose_axis_conflicts = bool(base.get("relax_non_pose_axis_conflicts"))
|
||||
base = {**empty_hardcore_position_config(), "enabled": True}
|
||||
base["restore_prompt_axes"] = restore_axes
|
||||
base["relax_non_pose_axis_conflicts"] = relax_non_pose_axis_conflicts
|
||||
else:
|
||||
base["enabled"] = True
|
||||
base["family"] = normalize_hardcore_position_family(family, base.get("family", "any"))
|
||||
selected = normalize_hardcore_position_values(selected_positions)
|
||||
if combine_mode == "add":
|
||||
existing = list(base.get("positions") or [])
|
||||
for value in selected:
|
||||
if value not in existing:
|
||||
existing.append(value)
|
||||
base["positions"] = existing
|
||||
else:
|
||||
base["positions"] = selected
|
||||
base["require_position"] = bool(base.get("require_position")) or bool(base["positions"]) or base["family"] != "any"
|
||||
base["summary"] = hardcore_position_summary(base)
|
||||
return json.dumps(base, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def build_hardcore_action_filter_json(
|
||||
hardcore_position_config: str | dict[str, Any] | None = "",
|
||||
focus: str = "keep_pool",
|
||||
allow_toys: bool = False,
|
||||
allow_double: bool = False,
|
||||
allow_penetration: bool = True,
|
||||
allow_foreplay: bool = True,
|
||||
allow_interaction: bool = True,
|
||||
allow_manual: bool = True,
|
||||
allow_oral: bool = True,
|
||||
allow_outercourse: bool = True,
|
||||
allow_anal: bool = True,
|
||||
allow_climax: bool = True,
|
||||
) -> str:
|
||||
config = parse_hardcore_position_config(hardcore_position_config)
|
||||
config["enabled"] = True
|
||||
focus = str(focus or "keep_pool").strip()
|
||||
focus_family = FOCUS_FAMILY_BY_KEY.get(focus)
|
||||
if focus_family:
|
||||
config["family"] = focus_family
|
||||
config["allow_toys"] = bool(allow_toys)
|
||||
config["allow_double"] = bool(allow_double)
|
||||
config["allow_penetration"] = bool(allow_penetration)
|
||||
config["allow_foreplay"] = bool(allow_foreplay)
|
||||
config["allow_interaction"] = bool(allow_interaction)
|
||||
config["allow_manual"] = bool(allow_manual)
|
||||
config["allow_oral"] = bool(allow_oral)
|
||||
config["allow_outercourse"] = bool(allow_outercourse)
|
||||
config["allow_anal"] = bool(allow_anal)
|
||||
config["allow_climax"] = bool(allow_climax)
|
||||
|
||||
if not focus_family and config["family"] != "any":
|
||||
enabled_action_families = {
|
||||
family
|
||||
for enabled, family in (
|
||||
(config["allow_penetration"], "penetrative"),
|
||||
(config["allow_foreplay"], "foreplay"),
|
||||
(config["allow_interaction"], "interaction"),
|
||||
(config["allow_manual"], "manual"),
|
||||
(config["allow_oral"], "oral"),
|
||||
(config["allow_outercourse"], "outercourse"),
|
||||
(config["allow_anal"], "anal"),
|
||||
(config["allow_climax"], "climax"),
|
||||
)
|
||||
if enabled
|
||||
}
|
||||
if config["family"] in enabled_action_families and len(enabled_action_families) > 1:
|
||||
config["family"] = "any"
|
||||
|
||||
if focus == "foreplay_only":
|
||||
config["allow_foreplay"] = True
|
||||
config["allow_interaction"] = True
|
||||
elif focus == "interaction_only":
|
||||
config["allow_interaction"] = True
|
||||
config["allow_foreplay"] = True
|
||||
elif focus == "manual_only":
|
||||
config["allow_manual"] = True
|
||||
elif focus == "oral_only":
|
||||
config["allow_oral"] = True
|
||||
config["allow_penetration"] = False
|
||||
elif focus == "outercourse_only":
|
||||
config["allow_outercourse"] = True
|
||||
config["allow_oral"] = False
|
||||
config["allow_penetration"] = False
|
||||
elif focus == "anal_only":
|
||||
config["allow_anal"] = True
|
||||
config["allow_penetration"] = True
|
||||
elif focus == "climax_only":
|
||||
config["allow_climax"] = True
|
||||
config["summary"] = hardcore_position_summary(config)
|
||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def hardcore_position_config_active(config: dict[str, Any]) -> bool:
|
||||
return bool(config.get("enabled"))
|
||||
|
||||
|
||||
def hardcore_position_template_required(config: dict[str, Any]) -> bool:
|
||||
if not hardcore_position_config_active(config):
|
||||
return False
|
||||
return (
|
||||
bool(config.get("require_position"))
|
||||
or bool(config.get("positions"))
|
||||
or normalize_hardcore_position_family(config.get("family")) != "any"
|
||||
)
|
||||
|
||||
|
||||
def hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
|
||||
family = normalize_hardcore_position_family(config.get("family"))
|
||||
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):
|
||||
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
|
||||
if not config.get("allow_foreplay", True):
|
||||
allowed.discard("foreplay_teasing")
|
||||
if not config.get("allow_interaction", True):
|
||||
allowed.difference_update(
|
||||
{
|
||||
"foreplay_teasing",
|
||||
"body_worship_touching",
|
||||
"clothing_position_transitions",
|
||||
"dominant_guidance",
|
||||
"camera_performance",
|
||||
"group_coordination",
|
||||
"aftercare_cleanup",
|
||||
}
|
||||
)
|
||||
if not config.get("allow_manual", True):
|
||||
allowed.discard("manual_stimulation")
|
||||
if not config.get("allow_oral", True):
|
||||
allowed.discard("oral_sex")
|
||||
if not config.get("allow_outercourse", True):
|
||||
allowed.discard("outercourse_sex")
|
||||
if not config.get("allow_anal", True):
|
||||
allowed.discard("anal_double_penetration")
|
||||
if not config.get("allow_climax", True):
|
||||
allowed.discard("cumshot_climax")
|
||||
if not config.get("allow_double", True) and family == "anal":
|
||||
allowed.add("anal_double_penetration")
|
||||
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:
|
||||
return (
|
||||
str(category.get("slug") or "").strip() == "hardcore_sexual_poses"
|
||||
or str(category.get("name") or "").strip().lower() == "hardcore sexual poses"
|
||||
)
|
||||
|
||||
|
||||
def hardcore_text_blocked_by_action(text: str, axis_name: str, config: dict[str, Any]) -> bool:
|
||||
text = str(text or "").lower()
|
||||
axis_name = str(axis_name or "").lower()
|
||||
if not config.get("allow_toys", True) and any(term in text for term in ("toy", "dildo", "strap-on", "strap on")):
|
||||
return True
|
||||
if not config.get("allow_double", True) and (
|
||||
axis_name == "double_act"
|
||||
or any(term in text for term in ("double penetration", "double-penetration", "front-and-back", "front and back", "second penetration", "both sides", "two partners penetrating", "multiple penetrations"))
|
||||
):
|
||||
return True
|
||||
if not config.get("allow_anal", True) and (
|
||||
axis_name == "anal_act"
|
||||
or any(term in text for term in (" anal", "anal sex", "anal penetration", "anus", "rear-entry anal", "penis entering ass", "thrusts into her ass", "thrusts into his ass"))
|
||||
):
|
||||
return True
|
||||
if not config.get("allow_oral", True) and (
|
||||
axis_name in ("oral_act", "oral_detail")
|
||||
or any(term in text for term in ("oral sex", "mouth on genitals", "mouth on pussy", "blowjob", "cunnilingus", "tongue on pussy", "deepthroat", "fellatio"))
|
||||
):
|
||||
return True
|
||||
if not config.get("allow_outercourse", True) and (
|
||||
axis_name in ("outer_act", "contact_detail", "texture_detail")
|
||||
or any(term in text for term in ("boobjob", "titjob", "breast sex", "breast-sex", "testicle", "balls", "penis licking", "penis-licking", "footjob", "soles", "toes"))
|
||||
):
|
||||
return True
|
||||
if not config.get("allow_penetration", True) and (
|
||||
axis_name in ("penetration_act", "penetration_detail", "anal_act", "double_act", "thrust_detail")
|
||||
or any(term in text for term in ("penetration", "penetrative", "thrust", "penis entering", "vaginal sex", "anal sex"))
|
||||
):
|
||||
return True
|
||||
if not config.get("allow_foreplay", True) and (
|
||||
axis_name in ("tease_act", "touch_detail", "clothing_detail", "foreplay_detail", "face_detail", "body_contact", "mood_detail")
|
||||
or any(
|
||||
term in text
|
||||
for term in (
|
||||
"kiss",
|
||||
"kissing",
|
||||
"mouth-to-mouth",
|
||||
"caress",
|
||||
"caressing",
|
||||
"stroking skin",
|
||||
"hands roaming",
|
||||
"touching breasts",
|
||||
"cupping breasts",
|
||||
"hand on the cheek",
|
||||
"fingers under the chin",
|
||||
"undressing",
|
||||
"removing clothing",
|
||||
"removing clothes",
|
||||
"pulling clothing",
|
||||
"sliding straps",
|
||||
"unbuttoning",
|
||||
)
|
||||
)
|
||||
):
|
||||
return True
|
||||
if not config.get("allow_interaction", True) and (
|
||||
axis_name
|
||||
in (
|
||||
"tease_act",
|
||||
"touch_detail",
|
||||
"clothing_detail",
|
||||
"foreplay_detail",
|
||||
"face_detail",
|
||||
"body_contact",
|
||||
"mood_detail",
|
||||
"worship_act",
|
||||
"transition_act",
|
||||
"control_act",
|
||||
"performance_act",
|
||||
"coordination_act",
|
||||
"aftercare_act",
|
||||
"cleanup_detail",
|
||||
)
|
||||
or any(
|
||||
term in text
|
||||
for term in (
|
||||
"kiss",
|
||||
"kissing",
|
||||
"caress",
|
||||
"body worship",
|
||||
"nipple",
|
||||
"ass grab",
|
||||
"thigh",
|
||||
"hair holding",
|
||||
"wrists",
|
||||
"dirty talk",
|
||||
"whispering",
|
||||
"undressing",
|
||||
"position transition",
|
||||
"guided",
|
||||
"camera",
|
||||
"watching",
|
||||
"aftercare",
|
||||
"cleanup",
|
||||
"wiping",
|
||||
)
|
||||
)
|
||||
):
|
||||
return True
|
||||
if not config.get("allow_manual", True) and (
|
||||
axis_name in ("manual_act", "manual_detail")
|
||||
or any(
|
||||
term in text
|
||||
for term in (
|
||||
"fingering",
|
||||
"fingers inside",
|
||||
"clit",
|
||||
"clitoris",
|
||||
"manual stimulation",
|
||||
"mutual masturbation",
|
||||
"masturbating together",
|
||||
"fingers on pussy",
|
||||
"fingers on clit",
|
||||
)
|
||||
)
|
||||
):
|
||||
return True
|
||||
if not config.get("allow_climax", True) and (
|
||||
axis_name in ("climax_act", "climax_hint", "climax_detail", "fluid_detail", "fluid_location")
|
||||
or any(term in text for term in ("climax", "cum", "semen", "ejaculat", "creampie", "post-orgasm", "post-penetration"))
|
||||
):
|
||||
return True
|
||||
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:
|
||||
positions = config.get("positions") or []
|
||||
if not positions:
|
||||
return True
|
||||
metadata_keys = _entry_position_keys(entry)
|
||||
if metadata_keys:
|
||||
return bool(set(metadata_keys) & set(positions))
|
||||
text = _entry_text(entry).lower()
|
||||
for position in positions:
|
||||
if _text_matches_position_key(text, position):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> bool:
|
||||
selected = set(config.get("positions") or [])
|
||||
if not selected:
|
||||
return False
|
||||
metadata_keys = _entry_position_keys(entry)
|
||||
if metadata_keys:
|
||||
matched = set(metadata_keys)
|
||||
else:
|
||||
text = _entry_text(entry).lower()
|
||||
matched = {
|
||||
position
|
||||
for position in HARDCORE_POSITION_KEY_MATCHES
|
||||
if _text_matches_position_key(text, position)
|
||||
}
|
||||
return bool(matched) and not bool(matched & selected)
|
||||
|
||||
|
||||
def restored_prompt_axis_relaxes_conflicts(axis_name: str, config: dict[str, Any]) -> bool:
|
||||
if str(axis_name or "") in HARDCORE_POSITION_AXIS_KEYS:
|
||||
return False
|
||||
if not config.get("relax_non_pose_axis_conflicts"):
|
||||
return False
|
||||
return str(axis_name or "") in set(normalize_restore_prompt_axes(config.get("restore_prompt_axes")))
|
||||
|
||||
|
||||
def hardcore_subcategory_supports_positions(subcategory: dict[str, Any], config: dict[str, Any]) -> bool:
|
||||
if not hardcore_position_template_required(config):
|
||||
return True
|
||||
axes = subcategory.get("item_axes")
|
||||
if not isinstance(axes, dict):
|
||||
return True
|
||||
for axis_name, values in axes.items():
|
||||
if str(axis_name) in HARDCORE_POSITION_AXIS_KEYS and any(
|
||||
hardcore_position_entry_matches(value, config)
|
||||
for value in _list_from(values)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def filter_hardcore_axis(axis_name: str, values: list[Any], config: dict[str, Any]) -> list[Any]:
|
||||
if not hardcore_position_config_active(config):
|
||||
return values
|
||||
filtered = [
|
||||
value
|
||||
for value in values
|
||||
if not hardcore_entry_blocked_by_action(value, axis_name, config)
|
||||
and not (
|
||||
axis_name not in HARDCORE_POSITION_AXIS_KEYS
|
||||
and not restored_prompt_axis_relaxes_conflicts(axis_name, config)
|
||||
and hardcore_position_entry_conflicts(value, config)
|
||||
)
|
||||
and (axis_name not in HARDCORE_POSITION_AXIS_KEYS or hardcore_position_entry_matches(value, config))
|
||||
]
|
||||
return filtered or values
|
||||
|
||||
|
||||
def filter_hardcore_templates(templates: list[Any], config: dict[str, Any]) -> list[Any]:
|
||||
if not hardcore_position_config_active(config):
|
||||
return templates
|
||||
filtered: list[Any] = []
|
||||
for template in templates:
|
||||
text = _entry_text(template)
|
||||
fields = {key for _, key, _, _ in Formatter().parse(text) if key}
|
||||
has_position_route = bool(fields & HARDCORE_POSITION_AXIS_KEYS) or bool(_entry_position_keys(template))
|
||||
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:
|
||||
filtered.append(template)
|
||||
return filtered or templates
|
||||
|
||||
|
||||
def apply_hardcore_position_config_to_subcategory(
|
||||
subcategory: dict[str, Any],
|
||||
config: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
if not hardcore_position_config_active(config):
|
||||
return subcategory
|
||||
subcategory_copy = dict(subcategory)
|
||||
if "item_templates" in subcategory_copy:
|
||||
subcategory_copy["item_templates"] = filter_hardcore_templates(_list_from(subcategory_copy["item_templates"]), config)
|
||||
raw_axes = subcategory_copy.get("item_axes")
|
||||
if isinstance(raw_axes, dict):
|
||||
axes = {}
|
||||
for axis_name, values in raw_axes.items():
|
||||
axes[axis_name] = filter_hardcore_axis(str(axis_name), _list_from(values), config)
|
||||
subcategory_copy["item_axes"] = axes
|
||||
subcategory_copy["hardcore_position_config"] = config
|
||||
return subcategory_copy
|
||||
|
||||
|
||||
def filter_hardcore_categories_for_position(
|
||||
categories: list[dict[str, Any]],
|
||||
config: dict[str, Any],
|
||||
women_count: int,
|
||||
men_count: int,
|
||||
compatible_entry: Callable[[dict[str, Any], int, int], bool],
|
||||
) -> list[dict[str, Any]]:
|
||||
if not hardcore_position_config_active(config):
|
||||
return categories
|
||||
allowed = hardcore_allowed_subcategory_slugs(config)
|
||||
filtered_categories: list[dict[str, Any]] = []
|
||||
for category in categories:
|
||||
if not is_hardcore_sexual_category(category):
|
||||
filtered_categories.append(category)
|
||||
continue
|
||||
category_copy = dict(category)
|
||||
subcategories = [
|
||||
subcategory
|
||||
for subcategory in category.get("subcategories", [])
|
||||
if str(subcategory.get("slug") or "") in allowed
|
||||
and compatible_entry(subcategory, women_count, men_count)
|
||||
and hardcore_subcategory_supports_positions(subcategory, config)
|
||||
]
|
||||
if subcategories:
|
||||
category_copy["subcategories"] = subcategories
|
||||
filtered_categories.append(category_copy)
|
||||
return filtered_categories
|
||||
|
||||
|
||||
def hardcore_source_position_family(subcategory: dict[str, Any], config: dict[str, Any] | None = None) -> str:
|
||||
slug = str(subcategory.get("slug") or subcategory.get("name") or "").strip().lower()
|
||||
family = HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY.get(slug, "")
|
||||
if family:
|
||||
return family
|
||||
config_family = normalize_hardcore_position_family((config or {}).get("family"), "")
|
||||
return "" if config_family == "any" else config_family
|
||||
|
||||
|
||||
def hardcore_position_keys(*parts: Any, axis_values: dict[str, Any] | None = None) -> list[str]:
|
||||
text = item_axis_policy.context_text(*parts, axis_values=axis_values)
|
||||
if not text:
|
||||
return []
|
||||
keys: list[str] = []
|
||||
for key in HARDCORE_POSITION_KEY_MATCHES:
|
||||
if _text_matches_position_key(text, key):
|
||||
keys.append(key)
|
||||
return keys
|
||||
|
||||
|
||||
_normalize_hardcore_position_family = normalize_hardcore_position_family
|
||||
_normalize_hardcore_position_values = normalize_hardcore_position_values
|
||||
_empty_hardcore_position_config = empty_hardcore_position_config
|
||||
_parse_hardcore_position_config = parse_hardcore_position_config
|
||||
_hardcore_position_summary = hardcore_position_summary
|
||||
_hardcore_position_config_active = hardcore_position_config_active
|
||||
_hardcore_position_template_required = hardcore_position_template_required
|
||||
_hardcore_allowed_subcategory_slugs = hardcore_allowed_subcategory_slugs
|
||||
_hardcore_source_position_family = hardcore_source_position_family
|
||||
_hardcore_position_keys = hardcore_position_keys
|
||||
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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:
|
||||
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||
|
||||
|
||||
def _anal_position_graph(woman: str, man: str, context: str) -> str:
|
||||
if "bent-over" in context or "bent over" in context:
|
||||
return f"{woman} is bent forward with hips raised while {man} stands behind her and thrusts his penis into her ass."
|
||||
if "face-down" in context:
|
||||
return f"{woman} lies face-down with ass raised while {man} is positioned behind her and thrusts his penis into her ass."
|
||||
if "doggy" in context or "rear-entry" in context:
|
||||
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
|
||||
if "standing" in context:
|
||||
return f"{woman} stands braced with hips angled back while {man} stands behind her and thrusts his penis into her ass."
|
||||
if "spooning" in context or "side-lying" in context:
|
||||
return f"{woman} lies on her side with thighs parted while {man} presses behind her and thrusts his penis into her ass."
|
||||
if "edge-of-bed" in context or "edge of bed" in context or "bed edge" in context or "edge-supported" in context:
|
||||
return f"{woman} lies near a raised edge with hips exposed while {man} kneels behind her and thrusts his penis into her ass."
|
||||
if "kneeling" in context:
|
||||
return f"{woman} kneels forward with hips raised while {man} kneels behind her and thrusts his penis into her ass."
|
||||
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
|
||||
|
||||
|
||||
def _two_person_double_graph(woman: str, man: str, context: str) -> str:
|
||||
if "bent-over" in context or "bent over" in context:
|
||||
return f"{woman} is bent forward with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
|
||||
if "face-down" in context:
|
||||
return f"{woman} lies face-down with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
|
||||
if "standing" in context:
|
||||
return f"{woman} stands braced with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
|
||||
if "kneeling" in context:
|
||||
return f"{woman} kneels forward with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
|
||||
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
|
||||
|
||||
|
||||
def build_anal_or_double_role_graph(
|
||||
woman: str,
|
||||
man: str,
|
||||
third: str,
|
||||
people_count: int,
|
||||
item_text: str,
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
context = _context_text(item_text, item_axis_values)
|
||||
if "double" in context or "toy" in context:
|
||||
if people_count >= 3 and third:
|
||||
return f"{man} thrusts his penis into {woman} while {third} adds a second penetration point from the front."
|
||||
return _two_person_double_graph(woman, man, context)
|
||||
if people_count >= 3 and third:
|
||||
return f"{man} thrusts his penis into {woman} while {third} gives oral contact from the front."
|
||||
return _anal_position_graph(woman, man, context)
|
||||
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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:
|
||||
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||
|
||||
|
||||
def _mentions_ass(text: str) -> bool:
|
||||
return bool(
|
||||
re.search(
|
||||
r"\bass\b|ass[- ](?:up|raised|exposed|lifted)|spread cheeks|lower back and ass|cum (?:on|dripping from) ass|pussy, ass|ass and",
|
||||
text,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def build_climax_role_graph(
|
||||
woman: str,
|
||||
man: str,
|
||||
third: str = "",
|
||||
item_text: str = "",
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
context = _context_text(item_text, item_axis_values)
|
||||
if "lying between two partners" in context and third:
|
||||
return f"{woman} lies between {man} and {third}, with {man} under her hips and {third} positioned above her torso as visible semen lands on her body."
|
||||
if "held between front-and-back partners" in context and third:
|
||||
return f"{woman} is held between {man} behind her and {third} in front of her as visible semen lands across her body."
|
||||
if "kneeling between standing partners" in context and third:
|
||||
return f"{woman} kneels between {man} and {third} while both stand close around her face and torso for visible ejaculation."
|
||||
if "side-lying with thighs parted" in context:
|
||||
return f"{woman} lies on her side with thighs parted while {man} kneels beside her hips and ejaculates semen across her thighs and pussy."
|
||||
if "sitting on the edge of the bed" in context:
|
||||
return f"{woman} sits on the edge of the bed with knees spread while {man} stands close between her legs and ejaculates semen across her body."
|
||||
if "lying at the bed edge with thighs open" in context:
|
||||
return f"{woman} lies at the bed edge with thighs open while {man} kneels between her legs and ejaculates semen across her pussy and thighs."
|
||||
if "reclining with thighs open" in context or "lying on the back with legs spread" in context:
|
||||
return (
|
||||
f"{woman} lies on her back with thighs open after ejaculation; thick semen and clear fluid cover her exposed pussy "
|
||||
f"and inner thighs as the exact-center aftermath detail, her body stays still, and her face and torso remain visible behind the open thighs."
|
||||
)
|
||||
if "on all fours with hips raised" in context:
|
||||
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and ejaculates semen across her ass, thighs, and lower back."
|
||||
if "face-down ass-up" in context:
|
||||
return f"{woman} lies face-down with ass raised while {man} is positioned behind her and ejaculates semen across her lower back and ass."
|
||||
if "bent over with ass raised" in context or "bent over" in context:
|
||||
return f"{woman} is bent forward with hips raised while {man} is positioned behind her, visible semen across her lower back, ass, and thighs."
|
||||
if "kneeling with mouth open" in context:
|
||||
return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest."
|
||||
if "kneeling in front of a standing partner" in context:
|
||||
return f"{woman} kneels in front of {man} at hip height while {man} stands over her and ejaculates semen across her body."
|
||||
if "standing with cum on the body" in context:
|
||||
return f"{woman} stands braced in front of {man} while he stays close at hip level and ejaculates semen across her body."
|
||||
if "squatting on top of a partner" in context:
|
||||
return f"{woman} squats over {man}'s hips while {man} lies on his back under her and ejaculates semen onto her body."
|
||||
if "reverse cowgirl over a partner's hips" in context:
|
||||
return f"{woman} straddles {man}'s hips facing away while {man} lies on his back under her and ejaculates semen onto her body."
|
||||
if any(term in context for term in ("straddling a partner", "straddling a partner's hips", "shared climax after penetration", "orgasm during penetration")):
|
||||
return f"{woman} straddles {man}'s hips while {man} lies on his back under her, their bodies still aligned from penetration as he ejaculates semen onto her body."
|
||||
if "seated in a partner's lap facing them" in context:
|
||||
return f"{woman} sits in {man}'s lap facing him, legs wrapped around his hips as he ejaculates semen across her body."
|
||||
if any(term in context for term in ("lower back", "cum dripping from ass", "cum on lower back")) or _mentions_ass(context):
|
||||
return f"{woman} is bent forward with hips raised while {man} is positioned behind her, visible semen across her lower back, ass, and thighs."
|
||||
if any(term in context for term in ("cum on face", "cum on tongue", "cum on lips", "cum on face and lips", "cum on tongue and chin")):
|
||||
if third:
|
||||
return f"{woman} kneels in the center while {man} and {third} stand close around her face and torso for visible ejaculation."
|
||||
return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest."
|
||||
return f"{woman} lies on her back with thighs open while {man} kneels between her legs and ejaculates semen onto her body."
|
||||
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from .hardcore_role_interaction import build_group_coordination_role_graph, build_manual_role_graph
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
from hardcore_role_interaction import build_group_coordination_role_graph, build_manual_role_graph
|
||||
|
||||
|
||||
def build_support_sentence(rng: random.Random, people: list[str], exclude: set[str]) -> str:
|
||||
extras = [person for person in people if person not in exclude]
|
||||
if not extras:
|
||||
return ""
|
||||
extra = rng.choice(extras)
|
||||
actions = [
|
||||
"kisses and grips the nearest body",
|
||||
"holds hips open for the camera",
|
||||
"touches breasts, thighs, and stomach",
|
||||
"keeps one hand on a partner's ass",
|
||||
"watches close and joins the body contact",
|
||||
"presses in from the side with hands on skin",
|
||||
]
|
||||
return f" {extra} {rng.choice(actions)}."
|
||||
|
||||
|
||||
def build_solo_role_graph(
|
||||
solo: str,
|
||||
women_count: int,
|
||||
slug: str,
|
||||
item_text: str = "",
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
if women_count == 1:
|
||||
if "manual_stimulation" in slug:
|
||||
return build_manual_role_graph(solo, item_text=item_text, item_axis_values=item_axis_values)
|
||||
if "camera_performance" in slug:
|
||||
return f"{solo} faces the camera and presents her body with hands framing the exposed skin in a solo creator-shot pose."
|
||||
if "cumshot" in slug or "climax" in slug:
|
||||
return f"{solo} is shown in a solo explicit orgasm pose with thighs open, one hand on her body, and visible arousal on skin and sheets."
|
||||
return f"{solo} is shown in a solo explicit adult pose with self-touch, open body framing, and direct camera awareness."
|
||||
if "cumshot" in slug or "climax" in slug:
|
||||
return f"{solo} is shown in a solo visible ejaculation pose with one hand on his penis, body angled toward the camera, and semen visible."
|
||||
return f"{solo} is shown in a solo explicit adult pose with direct camera awareness and clear body framing."
|
||||
|
||||
|
||||
def build_women_only_role_graph(
|
||||
slug: str,
|
||||
a: str,
|
||||
b: str,
|
||||
c: str = "",
|
||||
fallback_helper: str = "",
|
||||
item_text: str = "",
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
) -> tuple[str, set[str]]:
|
||||
used = {a, b}
|
||||
if "manual_stimulation" in slug:
|
||||
return build_manual_role_graph(a, b, item_text, item_axis_values), used
|
||||
if "group_coordination" in slug and c:
|
||||
used.add(c)
|
||||
return build_group_coordination_role_graph(a, b, c, item_text=item_text, item_axis_values=item_axis_values), used
|
||||
if "outercourse" in slug:
|
||||
return f"{a} kneels close to {b}'s body and uses mouth, hands, breasts, or feet for explicit non-penetrative contact.", used
|
||||
if "oral" in slug:
|
||||
return f"{a} kneels between {b}'s spread thighs and uses tongue and fingers on her pussy.", used
|
||||
if "anal" in slug or "double" in slug:
|
||||
return f"{a} uses a strap-on on {b} while keeping her hips held open.", used
|
||||
if "threesome" in slug or "group" in slug or "orgy" in slug:
|
||||
helper = c or fallback_helper or b
|
||||
used.add(helper)
|
||||
return f"{a} uses a strap-on on {b} while {helper} gives oral contact and touches both bodies.", used
|
||||
if "cumshot" in slug or "climax" in slug:
|
||||
return f"{a} brings {b} to orgasm with mouth and fingers while wetness is visible on thighs and sheets.", used
|
||||
return f"{a} uses a strap-on on {b} while their bodies stay pressed together.", used
|
||||
|
||||
|
||||
def build_men_only_role_graph(
|
||||
slug: str,
|
||||
a: str,
|
||||
b: str,
|
||||
c: str = "",
|
||||
fallback_helper: str = "",
|
||||
item_text: str = "",
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
) -> tuple[str, set[str]]:
|
||||
used = {a, b}
|
||||
if "manual_stimulation" in slug:
|
||||
return f"{a} and {b} sit or recline close together with hands visibly stimulating bodies in a manual sex setup.", used
|
||||
if "group_coordination" in slug and c:
|
||||
used.add(c)
|
||||
return build_group_coordination_role_graph(a, b, c, item_text=item_text, item_axis_values=item_axis_values), used
|
||||
if any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")):
|
||||
return f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside.", used
|
||||
if "outercourse" in slug:
|
||||
return f"{a} and {b} keep explicit non-penetrative penis contact visible with hands, mouth, or feet.", used
|
||||
if "oral" in slug:
|
||||
return f"{a} kneels and takes {b}'s penis in his mouth while holding his hips.", used
|
||||
if "anal" in slug or "double" in slug or "penetrative" in slug:
|
||||
return f"{a} penetrates {b} anally while {b}'s hips are held open.", used
|
||||
if "threesome" in slug or "group" in slug or "orgy" in slug:
|
||||
helper = c or fallback_helper or b
|
||||
used.add(helper)
|
||||
return f"{a} penetrates {b} anally while {helper} gives oral contact from the front.", used
|
||||
if "cumshot" in slug or "climax" in slug:
|
||||
return f"{a} ejaculates semen over {b}'s body while {b} keeps eye contact and one hand on his penis.", used
|
||||
return f"{a} and {b} keep explicit penis and anal contact visible.", used
|
||||
|
||||
|
||||
def build_mixed_group_fallback_role_graph(
|
||||
woman: str,
|
||||
man: str,
|
||||
third: str,
|
||||
helper: str,
|
||||
slug: str,
|
||||
) -> str:
|
||||
if "threesome" in slug:
|
||||
return f"{man} thrusts his penis into {woman} while {third or helper} uses mouth and hands on the exposed body."
|
||||
if "group" in slug or "orgy" in slug:
|
||||
return f"{man} thrusts his penis into {woman} while surrounding partners give oral contact and keep hands on hips, breasts, and thighs."
|
||||
return ""
|
||||
@@ -0,0 +1,176 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from . import item_axis_policy
|
||||
from .hardcore_role_anal import build_anal_or_double_role_graph
|
||||
from .hardcore_role_climax import build_climax_role_graph
|
||||
from .hardcore_role_fallback import (
|
||||
build_men_only_role_graph,
|
||||
build_mixed_group_fallback_role_graph,
|
||||
build_solo_role_graph,
|
||||
build_support_sentence,
|
||||
build_women_only_role_graph,
|
||||
)
|
||||
from .hardcore_role_interaction import (
|
||||
build_foreplay_role_graph,
|
||||
build_group_coordination_role_graph,
|
||||
build_interaction_role_graph,
|
||||
build_manual_role_graph,
|
||||
)
|
||||
from .hardcore_role_oral import build_oral_role_graph
|
||||
from .hardcore_role_outercourse import build_outercourse_role_graph
|
||||
from .hardcore_role_penetration import build_penetration_role_graph
|
||||
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_climax import build_climax_role_graph
|
||||
from hardcore_role_fallback import (
|
||||
build_men_only_role_graph,
|
||||
build_mixed_group_fallback_role_graph,
|
||||
build_solo_role_graph,
|
||||
build_support_sentence,
|
||||
build_women_only_role_graph,
|
||||
)
|
||||
from hardcore_role_interaction import (
|
||||
build_foreplay_role_graph,
|
||||
build_group_coordination_role_graph,
|
||||
build_interaction_role_graph,
|
||||
build_manual_role_graph,
|
||||
)
|
||||
from hardcore_role_oral import build_oral_role_graph
|
||||
from hardcore_role_outercourse import build_outercourse_role_graph
|
||||
from hardcore_role_penetration import build_penetration_role_graph
|
||||
|
||||
|
||||
def _lettered(prefix: str, count: int) -> list[str]:
|
||||
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
return [f"{prefix.capitalize()} {letters[index]}" for index in range(max(0, count))]
|
||||
|
||||
|
||||
def _pick_distinct(rng: random.Random, items: list[str], count: int) -> list[str]:
|
||||
if not items:
|
||||
return []
|
||||
if len(items) >= count:
|
||||
return rng.sample(items, count)
|
||||
picked = list(items)
|
||||
while len(picked) < count:
|
||||
picked.append(items[rng.randrange(len(items))])
|
||||
return picked
|
||||
|
||||
|
||||
def _participant_context(women_count: int, men_count: int) -> dict[str, list[str]]:
|
||||
women = _lettered("woman", women_count)
|
||||
men = _lettered("man", men_count)
|
||||
return {"women": women, "men": men, "people": women + men}
|
||||
|
||||
|
||||
def build_hardcore_role_graph(
|
||||
rng: random.Random,
|
||||
subcategory: dict[str, Any],
|
||||
context: dict[str, Any],
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
pov_labels: list[str] | None = None,
|
||||
) -> str:
|
||||
if context.get("subject_type") != "configured_cast":
|
||||
return ""
|
||||
women_count = int(context.get("women_count") or 0)
|
||||
men_count = int(context.get("men_count") or 0)
|
||||
people_count = women_count + men_count
|
||||
if people_count <= 0:
|
||||
return ""
|
||||
|
||||
participants = _participant_context(women_count, men_count)
|
||||
women = participants["women"]
|
||||
men = participants["men"]
|
||||
people = participants["people"]
|
||||
slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower()
|
||||
item_text = item_axis_policy.context_text(axis_values=item_axis_values)
|
||||
|
||||
def any_person(exclude: set[str] | None = None) -> str:
|
||||
exclude = exclude or set()
|
||||
pool = [person for person in people if person not in exclude] or people
|
||||
return rng.choice(pool)
|
||||
|
||||
def any_woman(exclude: set[str] | None = None) -> str:
|
||||
exclude = exclude or set()
|
||||
pool = [person for person in women if person not in exclude] or [person for person in people if person not in exclude] or people
|
||||
return rng.choice(pool)
|
||||
|
||||
def any_man(exclude: set[str] | None = None) -> str:
|
||||
exclude = exclude or set()
|
||||
pool = [person for person in men if person not in exclude] or [person for person in people if person not in exclude] or people
|
||||
return rng.choice(pool)
|
||||
|
||||
if people_count == 1:
|
||||
return build_solo_role_graph(people[0], women_count, slug, item_text, item_axis_values)
|
||||
|
||||
if women_count > 0 and men_count == 0:
|
||||
a, b = _pick_distinct(rng, women, 2)
|
||||
c = any_woman({a, b}) if len(women) >= 3 else ""
|
||||
used = {a, b}
|
||||
if any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")):
|
||||
graph = build_interaction_role_graph(a, b, c, slug, item_text, item_axis_values)
|
||||
if c and "camera_performance" in slug:
|
||||
used.add(c)
|
||||
elif "foreplay" in slug:
|
||||
graph = build_foreplay_role_graph(a, b, item_text, item_axis_values)
|
||||
else:
|
||||
graph, used = build_women_only_role_graph(
|
||||
slug,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
c or any_woman({a}),
|
||||
item_text,
|
||||
item_axis_values,
|
||||
)
|
||||
return graph + build_support_sentence(rng, people, used)
|
||||
|
||||
if men_count > 0 and women_count == 0:
|
||||
a, b = _pick_distinct(rng, men, 2)
|
||||
c = any_man({a, b}) if len(men) >= 3 else ""
|
||||
graph, used = build_men_only_role_graph(
|
||||
slug,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
c or any_man({a}),
|
||||
item_text,
|
||||
item_axis_values,
|
||||
)
|
||||
return graph + build_support_sentence(rng, people, used)
|
||||
|
||||
woman = any_woman()
|
||||
man = any_man()
|
||||
third = any_person({woman, man}) if people_count >= 3 else ""
|
||||
if "manual_stimulation" in slug:
|
||||
graph = build_manual_role_graph(woman, man, item_text, item_axis_values)
|
||||
elif "group_coordination" in slug:
|
||||
graph = build_group_coordination_role_graph(
|
||||
woman,
|
||||
man,
|
||||
third,
|
||||
any_person({woman, man}) if not third else "",
|
||||
item_text,
|
||||
item_axis_values,
|
||||
)
|
||||
elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")):
|
||||
graph = build_interaction_role_graph(woman, man, third, slug, item_text, item_axis_values)
|
||||
elif "foreplay" in slug:
|
||||
graph = build_foreplay_role_graph(woman, man, item_text, item_axis_values)
|
||||
elif "outercourse" in slug:
|
||||
graph = build_outercourse_role_graph(woman, man, item_text, item_axis_values, pov_labels)
|
||||
elif "oral" in slug:
|
||||
graph = build_oral_role_graph(woman, man, item_text, item_axis_values, pov_labels)
|
||||
elif "anal" in slug or "double" in slug:
|
||||
graph = build_anal_or_double_role_graph(woman, man, third, people_count, item_text, item_axis_values)
|
||||
elif "threesome" in slug or "group" in slug or "orgy" in slug:
|
||||
graph = build_mixed_group_fallback_role_graph(woman, man, third, any_person({woman, man}), slug)
|
||||
elif "cumshot" in slug or "climax" in slug:
|
||||
graph = build_climax_role_graph(woman, man, third, item_text, item_axis_values)
|
||||
else:
|
||||
graph = build_penetration_role_graph(woman, man, item_text, item_axis_values)
|
||||
return graph + build_support_sentence(rng, people, {woman, man, third} if third else {woman, man})
|
||||
@@ -0,0 +1,146 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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:
|
||||
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||
|
||||
|
||||
def build_foreplay_role_graph(
|
||||
primary: str,
|
||||
partner: str,
|
||||
item_text: str,
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
text = _context_text(item_text, item_axis_values)
|
||||
if any(term in text for term in ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning")):
|
||||
return (
|
||||
f"{primary} and {partner} stand close while {partner}'s hands pull clothing aside from {primary}'s body; "
|
||||
f"{primary}'s exposed skin and the clothing being removed stay clearly visible."
|
||||
)
|
||||
if any(term in text for term in ("breast", "breasts", "nipple", "cupping breasts", "touching breasts")):
|
||||
return (
|
||||
f"{primary} and {partner} press their bodies close while {partner}'s hand cups {primary}'s breast; "
|
||||
f"their faces stay close and the breast-touching gesture is clear."
|
||||
)
|
||||
if any(term in text for term in ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin")):
|
||||
return (
|
||||
f"{primary} and {partner} stand face-to-face at close range while one hand holds {primary}'s cheek and jaw; "
|
||||
f"their lips are close and the face-touching gesture is clear."
|
||||
)
|
||||
if any(term in text for term in ("kiss", "kissing", "mouth-to-mouth", "lips pressed")):
|
||||
return (
|
||||
f"{primary} and {partner} press their bodies together and kiss deeply, "
|
||||
f"with hands on each other's face, waist, and hips."
|
||||
)
|
||||
return (
|
||||
f"{primary} and {partner} are pressed close in a heated foreplay setup, "
|
||||
f"hands caressing skin while clothing is pulled aside."
|
||||
)
|
||||
|
||||
|
||||
def build_manual_role_graph(
|
||||
primary: str,
|
||||
partner: str = "",
|
||||
item_text: str = "",
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
text = _context_text(item_text, item_axis_values)
|
||||
if not partner:
|
||||
if "mutual" in text:
|
||||
return f"{primary} faces the camera with thighs open, both hands on her body for solo mutual-style masturbation framing."
|
||||
return f"{primary} reclines with thighs open, one hand between her legs and fingers visibly stimulating her pussy."
|
||||
if "mutual" in text:
|
||||
return f"{primary} and {partner} sit close facing each other, both touching themselves while keeping hands, faces, and bodies visible."
|
||||
if "toy" in text or "vibrator" in text or "wand" in text:
|
||||
return (
|
||||
f"{primary} reclines in a close first-person toy-contact view with thighs spread wide toward the camera; "
|
||||
"a single continuous teal wand-style massager is the largest lower-frame object, "
|
||||
"the rounded bulb head presses flat to her vulva and clit as the central contact point, "
|
||||
f"and the smooth handle angles in from the bottom right inside {partner}'s visible hand. "
|
||||
f"{primary}'s open thighs and knees form a V around the foreground wand while her face and torso remain visible behind the leg frame."
|
||||
)
|
||||
if "clit" in text or "clitoris" in text:
|
||||
return (
|
||||
f"{primary} reclines with thighs open while {partner}'s foreground hand is the largest lower-frame object; "
|
||||
f"her open thighs form a V around the hand, the wrist enters from the bottom center, "
|
||||
f"and two fingers press at her vulva and clit as the clear contact point while her face and torso remain visible behind the open thighs."
|
||||
)
|
||||
return (
|
||||
f"{primary} reclines with thighs open while {partner}'s foreground hand is the largest lower-frame object; "
|
||||
f"her open thighs form a V around the hand, the wrist enters from the bottom center, "
|
||||
f"and two fingers at her vulva and clit make the central manual-contact point while her face and torso remain visible behind the open thighs."
|
||||
)
|
||||
|
||||
|
||||
def build_interaction_role_graph(
|
||||
primary: str,
|
||||
partner: str,
|
||||
third: str = "",
|
||||
slug: str = "",
|
||||
item_text: str = "",
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
text = _context_text(item_text, item_axis_values)
|
||||
if "aftercare" in slug or any(term in text for term in ("aftercare", "cleanup", "wiping", "towel", "post-sex", "cuddle")):
|
||||
if "cleanup" in text or "wiping" in text or "towel" in text:
|
||||
return f"{primary} reclines after sex while {partner} kneels close and wipes her skin with a towel, hands and relaxed body contact visible."
|
||||
return f"{primary} and {partner} lie close together after sex, bodies relaxed and hands resting on skin in a post-sex cuddle."
|
||||
if "camera_performance" in slug or any(term in text for term in ("camera", "presenting", "showing", "viewer", "creator-shot")):
|
||||
if any(term in text for term in ("spread open", "open thighs", "thighs open", "legs spread", "knees held wide")):
|
||||
return (
|
||||
f"{primary} sits back facing the camera with knees raised and held wide; "
|
||||
f"her thighs form a broad V-frame around her centered exposed vulva, "
|
||||
f"her hands hold her knees and upper thighs, and her face and torso remain visible behind the open-thigh frame."
|
||||
)
|
||||
if third:
|
||||
return f"{primary} faces the camera while {partner} and {third} hold and present her body, hands framing the exposed skin for the viewer."
|
||||
return f"{primary} faces the camera and presents her body while {partner}'s hands hold her hips or thighs open for a clear creator-shot reveal."
|
||||
if "body_worship" in slug or any(term in text for term in ("body worship", "nipple", "thigh", "mouth on skin", "kissing down", "ass grabbing")):
|
||||
if "ass" in text:
|
||||
return f"{primary} stands or kneels with hips angled back while {partner}'s hands grip her ass, fingers pressing into skin."
|
||||
if "thigh" in text:
|
||||
return f"{primary} reclines with thighs open while {partner} kneels close and kisses along her inner thighs, hands holding her legs in place."
|
||||
if "nipple" in text or "breast" in text:
|
||||
return f"{primary} arches toward {partner} while {partner}'s mouth is on her breast and one hand cups or squeezes the other breast."
|
||||
return f"{primary} reclines or leans back while {partner} kisses down her body, hands tracing breasts, waist, hips, and thighs."
|
||||
if "clothing_position" in slug or any(term in text for term in ("transition", "turning", "pulling onto", "lifting", "guided backward", "clothing", "garment")):
|
||||
if "turn" in text or "rear-facing" in text:
|
||||
return f"{partner}'s hands turn {primary} around by the hips, clothing partly moved aside as her body rotates into the next pose."
|
||||
if "legs" in text or "thigh" in text:
|
||||
return f"{primary} lies back while {partner} lifts and spreads her legs into position, hands and clothing movement clearly visible."
|
||||
return f"{primary} and {partner} are mid-transition, with {partner}'s hands moving clothing aside and guiding {primary}'s hips toward the next pose."
|
||||
if "dominant" in slug or any(term in text for term in ("hair", "wrist", "wrists", "jaw", "chin", "guided", "dominant", "control", "dirty talk", "whisper", "mouth near the ear", "verbal teasing")):
|
||||
if "dirty talk" in text or "whisper" in text or "mouth near the ear" in text or "verbal teasing" in text:
|
||||
return f"{partner} leans close to {primary}'s ear for dirty talk while holding her waist and keeping their bodies pressed close."
|
||||
if "wrist" in text or "wrists" in text:
|
||||
return f"{primary} lies back while {partner} pins her wrists above her head, both bodies close and the consensual control gesture clearly visible."
|
||||
if "hair" in text:
|
||||
return f"{partner} holds {primary}'s hair back while guiding her body closer, face and hair-hold gesture visible."
|
||||
if "thigh" in text or "spread" in text:
|
||||
return f"{primary} reclines with thighs open while {partner}'s hands spread her legs and hold the position for the camera."
|
||||
return f"{partner} guides {primary}'s body with hands on her jaw, waist, and hips, keeping the consensual control gesture readable."
|
||||
return build_foreplay_role_graph(primary, partner, item_text, item_axis_values)
|
||||
|
||||
|
||||
def build_group_coordination_role_graph(
|
||||
primary: str,
|
||||
partner: str,
|
||||
third: str = "",
|
||||
fallback_observer: str = "",
|
||||
item_text: str = "",
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
observer = third or fallback_observer or partner
|
||||
text = _context_text(item_text, item_axis_values)
|
||||
if "camera" in text or "hold" in text or "present" in text:
|
||||
return f"{primary} is centered while {partner} and {observer} hold and present the body for the camera, each role clearly visible."
|
||||
if "watch" in text or "waiting" in text:
|
||||
return f"{primary} is centered while {partner} touches her body and {observer} watches close beside them, hands and faces readable."
|
||||
return f"{primary} is centered while {partner} touches her body and {observer} stays close as the watching or guiding partner."
|
||||
@@ -0,0 +1,186 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from . import item_axis_policy
|
||||
except ImportError: # Allows local smoke tests with top-level imports.
|
||||
import item_axis_policy
|
||||
|
||||
|
||||
SITTING_ORAL_VARIANT = "pov_blowjob_sitting_upright_oral"
|
||||
|
||||
|
||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||
|
||||
|
||||
def _oral_direction(text: str) -> tuple[bool, bool]:
|
||||
woman_gives = any(
|
||||
term in text
|
||||
for term in (
|
||||
"fellatio",
|
||||
"blowjob",
|
||||
"deepthroat",
|
||||
"penis sucking",
|
||||
"penis in mouth",
|
||||
"penis in her mouth",
|
||||
"mouth stretched around a penis",
|
||||
"lips wrapped",
|
||||
)
|
||||
)
|
||||
man_gives = any(
|
||||
term in text
|
||||
for term in (
|
||||
"cunnilingus",
|
||||
"pussy licking",
|
||||
"tongue on pussy",
|
||||
"mouth on pussy",
|
||||
"pussy and tongue",
|
||||
"face-sitting",
|
||||
"tongue contact clearly visible",
|
||||
)
|
||||
)
|
||||
if "mouth on genitals" in text and not woman_gives and not man_gives:
|
||||
if any(term in text for term in ("face-sitting", "reclining", "straddled", "spread-leg", "open thighs")):
|
||||
man_gives = True
|
||||
else:
|
||||
woman_gives = True
|
||||
return woman_gives, man_gives
|
||||
|
||||
|
||||
def _list_values(value: Any) -> list[str]:
|
||||
if isinstance(value, list):
|
||||
return [str(item) for item in value if str(item).strip()]
|
||||
if isinstance(value, str) and value.strip():
|
||||
return [part.strip().strip("[]'\" ") for part in value.split(",") if part.strip().strip("[]'\" ")]
|
||||
return []
|
||||
|
||||
|
||||
def _has_krea2_variant(axis_values: dict[str, Any] | None, key: str) -> bool:
|
||||
if not isinstance(axis_values, dict):
|
||||
return False
|
||||
return key in _list_values(axis_values.get("krea2_variant_keys"))
|
||||
|
||||
|
||||
def build_oral_role_graph(
|
||||
woman: str,
|
||||
man: str,
|
||||
item_text: str,
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
pov_labels: list[str] | None = None,
|
||||
) -> str:
|
||||
position_text = item_axis_policy.key_text(item_axis_values, "position")
|
||||
text = _context_text(item_text, item_axis_values)
|
||||
man_is_pov = man in set(pov_labels or [])
|
||||
woman_gives, man_gives = _oral_direction(text)
|
||||
|
||||
if _has_krea2_variant(item_axis_values, SITTING_ORAL_VARIANT):
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"The viewer reclines with open thighs in an upright sitting oral frame while {woman} sits low between his thighs; "
|
||||
f"{woman}'s face lowers close to the centered shaft tip, her mouth on the viewer's penis, with both hands low at the base."
|
||||
)
|
||||
return (
|
||||
f"{man} reclines with open thighs in an upright sitting oral frame while {woman} sits low between his thighs; "
|
||||
f"{woman}'s face lowers close to the centered shaft tip, her mouth on his penis, with both hands low at the base."
|
||||
)
|
||||
if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text):
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} and the viewer lie head-to-hips in a sixty-nine position, "
|
||||
f"with {woman}'s mouth on the viewer's penis and the viewer's mouth on {woman}'s pussy."
|
||||
)
|
||||
return f"{woman} and {man} lie head-to-hips in a sixty-nine position, with {woman}'s mouth on {man}'s penis and {man}'s mouth on {woman}'s pussy."
|
||||
if "face-sitting" in position_text or ("face-sitting" in text and not position_text):
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} is above the POV camera, straddling the viewer's face with thighs on both sides of his head, "
|
||||
"pussy directly over the viewer's mouth for close first-person underview tongue contact."
|
||||
)
|
||||
return f"{man} lies on his back while {woman} straddles his face with her thighs around his head and {man}'s mouth pressed to her pussy."
|
||||
if "straddled oral" in position_text or ("straddled oral" in text and not position_text):
|
||||
if woman_gives and not man_gives:
|
||||
if man_is_pov:
|
||||
return f"The viewer straddles forward near {woman}'s face while {woman} kneels below him with her mouth on his penis."
|
||||
return f"{man} straddles forward near {woman}'s face while {woman} kneels below him with her mouth on his penis."
|
||||
if man_is_pov:
|
||||
return f"{woman} straddles above the viewer's face with her thighs framing his head while the viewer's mouth stays pressed to her pussy."
|
||||
return f"{woman} straddles above {man}'s face with her thighs framing his head while {man}'s mouth stays pressed to her pussy."
|
||||
if "side-lying oral" in position_text or ("side-lying oral" in text and not position_text):
|
||||
if woman_gives and not man_gives:
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"The adult male viewer reclines in a first-person side-profile body-line view with the adult male viewer's abdomen, "
|
||||
"navel, pelvis, and near thigh creating a broad horizontal body surface; the adult male viewer's own torso starts at the lower edge "
|
||||
f"and runs diagonally into the lower-right foreground, with navel, abdomen hair, pelvis, and near thigh marking the camera owner's body; {woman} enters laterally from the left edge beside his hip, "
|
||||
"cheek and jaw in profile, mouth on the shaft at the male abdomen line, lips touching the shaft at the male abdomen line, "
|
||||
"mouth-to-shaft contact is the nearest facial detail, hand around the base under her lips, "
|
||||
"shoulder and torso trailing sideways along the edge."
|
||||
)
|
||||
return f"{man} lies on his side with hips angled toward {woman} while {woman} lies beside his thighs and takes his penis in her mouth."
|
||||
if man_is_pov:
|
||||
return f"{woman} lies on her side with her top thigh lifted while the viewer lies beside her hips with his mouth pressed to her pussy."
|
||||
return f"{woman} lies on her side with her top thigh lifted while {man} lies beside her hips with his mouth pressed to her pussy."
|
||||
if (
|
||||
"edge-of-bed oral" in position_text
|
||||
or "edge of bed oral" in position_text
|
||||
or "edge-supported oral" in position_text
|
||||
or (("edge-of-bed oral" in text or "edge of bed oral" in text or "edge-supported oral" in text) and not position_text)
|
||||
):
|
||||
if woman_gives and not man_gives:
|
||||
if man_is_pov:
|
||||
return f"The viewer sits at a raised edge with legs apart while {woman} kneels between his thighs and takes the viewer's penis in her mouth."
|
||||
return f"{man} sits at a raised edge with legs apart while {woman} kneels between his thighs and takes his penis in her mouth."
|
||||
if man_is_pov:
|
||||
return f"{woman} lies at a raised edge with thighs open while the viewer kneels between her legs with his mouth on her pussy."
|
||||
return f"{woman} lies at a raised edge with thighs open while {man} kneels between her legs with his mouth on her pussy."
|
||||
if "standing oral" in position_text or ("standing oral" in text and not position_text):
|
||||
if man_gives and not woman_gives:
|
||||
if man_is_pov:
|
||||
return f"{woman} stands braced with one thigh lifted while the viewer kneels between her legs with his mouth on her pussy."
|
||||
return f"{woman} stands braced with one thigh lifted while {man} kneels between her legs with his mouth on her pussy."
|
||||
if man_is_pov:
|
||||
return f"The viewer stands with hips forward while {woman} kneels in front of him at hip height and takes the viewer's penis in her mouth."
|
||||
return f"{man} stands with hips forward while {woman} kneels in front of him at hip height and takes his penis in her mouth."
|
||||
if "chair oral" in position_text or ("chair oral" in text and not position_text):
|
||||
if man_gives and not woman_gives:
|
||||
if man_is_pov:
|
||||
return f"{woman} sits in a chair with thighs open while the viewer kneels between her legs with his mouth pressed to her pussy."
|
||||
return f"{woman} sits in a chair with thighs open while {man} kneels between her legs with his mouth pressed to her pussy."
|
||||
if man_is_pov:
|
||||
return f"The viewer sits in a chair with legs apart while {woman} kneels between his thighs and takes the viewer's penis in her mouth."
|
||||
return f"{man} sits in a chair with legs apart while {woman} kneels between his thighs and takes his penis in her mouth."
|
||||
if (
|
||||
"reclining cunnilingus" in position_text
|
||||
or "spread-leg oral" in position_text
|
||||
or (("reclining cunnilingus" in text or "spread-leg oral" in text) and not position_text)
|
||||
):
|
||||
if woman_gives and not man_gives:
|
||||
if man_is_pov:
|
||||
return f"The viewer reclines with legs apart while {woman} kneels between his thighs and takes the viewer's penis in her mouth."
|
||||
return f"{man} reclines with legs apart while {woman} kneels between his thighs and takes his penis in her mouth."
|
||||
if man_is_pov:
|
||||
return f"{woman} reclines on her back with thighs spread while the viewer kneels between her legs with his mouth on her pussy."
|
||||
return f"{woman} reclines on her back with thighs spread while {man} kneels between her legs with his mouth on her pussy."
|
||||
if "kneeling oral" in position_text or ("kneeling oral" in text and not position_text):
|
||||
if man_gives and not woman_gives:
|
||||
if man_is_pov:
|
||||
return f"{woman} kneels with thighs parted and hips angled forward while the viewer kneels in front of her with his mouth on her pussy."
|
||||
return f"{woman} kneels with thighs parted and hips angled forward while {man} kneels in front of her with his mouth on her pussy."
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} kneels in front of the viewer's penis while he stands over her; "
|
||||
f"{woman} takes the viewer's penis in her mouth with saliva dripping on the penis as he looks down toward her."
|
||||
)
|
||||
return (
|
||||
f"{woman} kneels in front of {man}'s penis while {man} stands over her; "
|
||||
f"{woman} takes {man}'s penis in her mouth with saliva dripping on the penis as {man} looks down toward her."
|
||||
)
|
||||
if man_gives and not woman_gives:
|
||||
if man_is_pov:
|
||||
return f"{woman} lies on her back with thighs open while the viewer kneels between her legs with his mouth pressed to her pussy."
|
||||
return f"{woman} lies on her back with thighs open while {man} kneels between her legs with his mouth pressed to her pussy."
|
||||
if man_is_pov:
|
||||
return f"{woman} kneels in front of the viewer's hips and takes the viewer's penis in her mouth while he keeps his hips aligned with her face."
|
||||
return f"{woman} kneels in front of {man}'s hips and takes his penis in her mouth while {man} keeps his hips aligned with her face."
|
||||
@@ -0,0 +1,117 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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:
|
||||
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||
|
||||
|
||||
def build_outercourse_role_graph(
|
||||
woman: str,
|
||||
man: str,
|
||||
item_text: str,
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
pov_labels: list[str] | None = None,
|
||||
) -> str:
|
||||
position_text = item_axis_policy.key_text(item_axis_values, "position")
|
||||
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 [])
|
||||
if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} kneels low between the POV viewer's open thighs with her torso bent forward over his pelvis, "
|
||||
"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 (
|
||||
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 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 action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} lies low in a side-pelvis POV beside the POV viewer's open thighs, "
|
||||
"her face is the closest visible partner part, her cheek against the POV viewer's inner thigh and her head low under his pelvis, "
|
||||
"with the POV viewer's scrotum at her mouth; scrotum is the mouth surface, testicles resting across her open lips while her tongue cups them from below, "
|
||||
"scrotal skin is the nearest mouth surface and both testicles rest against her tongue from below, "
|
||||
"and his abdomen and inner thighs frame the close foreground."
|
||||
)
|
||||
return (
|
||||
f"{man} reclines with legs apart while {woman} lies low beside his inner thigh, "
|
||||
f"her face as the closest visible partner part, her cheek against his thigh and her head low under his pelvis, "
|
||||
f"with {man}'s scrotum at her mouth; scrotum is the mouth surface, scrotal skin is the nearest mouth surface, and testicles resting across her open lips while both testicles rest against her tongue from below."
|
||||
)
|
||||
if action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING:
|
||||
prone_laying = any(
|
||||
term in f"{position_text} {text}"
|
||||
for term in ("reclining", "prone", "belly-down", "belly down", "lying")
|
||||
)
|
||||
if prone_laying:
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} lies belly-down between the POV viewer's open thighs while his thighs form a wide V-frame in the foreground, "
|
||||
"her torso stretched low and horizontal between his knees, her front-facing mouth and tongue aligned to the POV viewer's penis, "
|
||||
"and her hands wrap the base of the POV viewer's penis."
|
||||
)
|
||||
return (
|
||||
f"{woman} lies belly-down between {man}'s open thighs while his thighs form a wide V-frame in the foreground, "
|
||||
f"her torso stretched low and horizontal between his knees, her front-facing mouth and tongue aligned to {man}'s penis, "
|
||||
f"and her hands wrap the base of {man}'s penis."
|
||||
)
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} bends forward between the POV viewer's open thighs with her head low under the POV viewer's 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 (
|
||||
f"{woman} bends forward between {man}'s open thighs with her head low under {man}'s 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 action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} faces the POV viewer with hips back, torso visible behind her raised legs, and both knees bent open toward the camera, "
|
||||
"while two large overlapping soles dominate the POV viewer's lower center foreground and clamp the POV viewer's upright shaft between them. "
|
||||
"Her inner arches press inward from both sides, toes curl around both edges, a narrow visible strip of shaft and glans rises between the compressed feet, "
|
||||
"and her face and torso stay visible behind the large foreground feet."
|
||||
)
|
||||
return (
|
||||
f"{man} reclines with hips forward while {woman} faces him with her hips back and both knees bent open, "
|
||||
f"two large overlapping soles dominating the lower center foreground as her inner arches press inward around {man}'s upright shaft, "
|
||||
"her toes curl around both edges, and a narrow visible strip of shaft and glans rises between the compressed feet."
|
||||
)
|
||||
if action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
|
||||
if man_is_pov:
|
||||
return (
|
||||
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 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 (
|
||||
f"{woman} kneels between {man}'s open thighs with her torso leaning forward and face visible behind {man}'s penis, "
|
||||
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:
|
||||
return (
|
||||
f"{woman} kneels close to the POV viewer's hips and keeps the POV viewer's penis centered in clear non-penetrative contact, "
|
||||
"with her mouth, hands, breasts, or feet visibly working around the penis shaft."
|
||||
)
|
||||
return (
|
||||
f"{woman} kneels close to {man}'s hips and keeps {man}'s penis centered in clear non-penetrative contact, "
|
||||
"with her mouth, hands, breasts, or feet visibly working around the penis shaft."
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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:
|
||||
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||
|
||||
|
||||
def build_penetration_role_graph(
|
||||
woman: str,
|
||||
man: str,
|
||||
item_text: str,
|
||||
item_axis_values: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
text = _context_text(item_text, item_axis_values)
|
||||
if "folded missionary" in text or "knees-to-chest" in text or "knees to chest" in text:
|
||||
return (
|
||||
f"{woman} lies on her back facing {man} with knees folded high toward her chest while {man} is above her between her thighs; "
|
||||
f"{man}'s hands hold her calves and {man}'s penis thrusts into her pussy below the raised knees."
|
||||
)
|
||||
if "missionary" in text:
|
||||
return (
|
||||
f"{woman} lies on her back with legs open around {man}'s hips while {man} is above her between her thighs; "
|
||||
f"{man}'s hips press close and {man}'s penis thrusts into her pussy."
|
||||
)
|
||||
if "cowgirl-alt" in text or "low cowgirl" in text or "seated-squat cowgirl" in text or "low seated squat" in text:
|
||||
return (
|
||||
f"{woman} faces {man} in a low seated squat over {man}'s pelvis while {man} lies flat on his back under her; "
|
||||
f"{man} supports the underside of her thighs and {man}'s penis thrusts into her pussy."
|
||||
)
|
||||
if "reverse cowgirl alt" in text or "upright reverse cowgirl" in text or "upright back-facing straddle" in text:
|
||||
return (
|
||||
f"{woman} sits upright facing away in a back-facing straddle over {man}'s pelvis while {man} lies under her; "
|
||||
f"{man}'s hands hold her hips and {man}'s penis thrusts into her pussy."
|
||||
)
|
||||
if "reverse cowgirl" in text:
|
||||
return f"{woman} straddles {man}'s hips facing away while {man} lies under her and {man}'s penis thrusts into her pussy."
|
||||
if "cowgirl" in text or "straddling" in text:
|
||||
return (
|
||||
f"{woman} straddles {man}'s hips facing him while {man} lies under her; "
|
||||
f"{man}'s lower abdomen and pelvis anchor the bottom edge, {woman}'s thighs form a wide horizontal thigh bridge from left edge to right edge, "
|
||||
f"her knees plant outside {man}'s hips, {man}'s hands grip the sides of her thighs, and centered contact remains below her belly as {man}'s penis thrusts into her pussy."
|
||||
)
|
||||
if "doggy" in text or "rear-entry" in text or "bent-over" in text or "bent over" in text:
|
||||
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and {man}'s penis thrusts into her pussy."
|
||||
if "standing" in text:
|
||||
return f"{woman} stands braced with hips angled back while {man} stands behind her and {man}'s penis thrusts into her pussy."
|
||||
if "spooning" in text or "side-lying" in text:
|
||||
return f"{woman} lies on her side with thighs parted while {man} presses behind her and {man}'s penis thrusts into her pussy."
|
||||
if "edge-of-bed" in text or "edge of bed" in text or "bed edge" in text or "edge-supported" in text or "raised edge" in text:
|
||||
return (
|
||||
f"{woman} lies back at a raised edge with hips at the edge and legs open while {man} kneels between her thighs; "
|
||||
f"{man}'s hips press close and {man}'s penis thrusts into her pussy."
|
||||
)
|
||||
if "kneeling straddle" in text:
|
||||
return f"{woman} kneels straddling {man}'s hips while {man} supports her waist and {man}'s penis thrusts into her pussy."
|
||||
if "lotus" in text:
|
||||
return f"{woman} sits in {man}'s lap facing him with legs around his hips while {man}'s penis thrusts into her pussy."
|
||||
return (
|
||||
f"{woman} lies on her back with legs spread wide and knees bent outward while {man} kneels between her open thighs facing her; "
|
||||
f"{man}'s hips are pressed between her legs and {man}'s penis thrusts into her pussy."
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
HARDCORE_ENVIRONMENT_ANCHOR_REPLACEMENTS = (
|
||||
(r"\bon against a wall\b", "against a wall"),
|
||||
(r"\bstacked bodies on the bed\b", "close body alignment"),
|
||||
(r"\bstacked bodies with close body alignment\b", "close body alignment"),
|
||||
(r"\boverhead tangled-body anal frame\b", "overhead rear-entry anal frame"),
|
||||
(r"\btangled-body\b", "close-body"),
|
||||
(r"\bthree bodies tangled on the bed\b", "three bodies tangled in close contact"),
|
||||
(r"\ba triangle of bodies on the mattress\b", "a triangle of bodies in close contact"),
|
||||
(r"\bbodies tangled on the sheets\b", "bodies tangled in close contact"),
|
||||
(r"\bwet bodies tangled on sheets\b", "wet bodies tangled in close contact"),
|
||||
(r"\bbody arched on rumpled sheets\b", "body arched with clear skin contact"),
|
||||
(r"\bface-down ass-up on the mattress\b", "face-down ass-up position"),
|
||||
(r"\bsitting on the edge of the bed\b", "sitting on a raised edge"),
|
||||
(r"\blying at the bed edge with thighs open\b", "lying near a raised edge with thighs open"),
|
||||
(r"\bedge[- ]of[- ]bed\b", "edge-supported"),
|
||||
(r"\bbed[- ]edge\b", "raised edge"),
|
||||
(r"\bedge of (?:the )?bed\b", "raised edge"),
|
||||
(r"\bbed edge\b", "raised edge"),
|
||||
(r"\bhands? braced on the bed\b", "hands braced beside the body"),
|
||||
(r"\bone hand pressing into the mattress\b", "one hand braced beside the body"),
|
||||
(r"\bone foot planted on the bed\b", "one foot planted for leverage"),
|
||||
(r"\bfingers gripping sheets and skin\b", "fingers gripping skin"),
|
||||
(r"\bfingers gripping sheets\b", "fingers gripping skin"),
|
||||
(r"\bhands gripping sheets\b", "hands gripping skin"),
|
||||
(r"\bone hand gripping the sheets\b", "one hand gripping skin"),
|
||||
(r"\brumpled bed sheets\b", "wrinkled body-contact fabric"),
|
||||
(r"\bwet sheets beneath the bodies\b", "visible wetness beneath the bodies"),
|
||||
(r"\bsexual fluids on sheets\b", "sexual fluids visible on skin"),
|
||||
(r"\bcum dripping onto sheets\b", "cum visible on skin"),
|
||||
(r"\bfluid dripping onto sheets\b", "fluid visible on skin"),
|
||||
(r"\bsquirting fluid on the sheets\b", "squirting fluid visible on skin"),
|
||||
(r"\bsoft sheets\b", "soft fabric"),
|
||||
(r"\bsilk sheets\b", "silk fabric"),
|
||||
(r"\bsheets\b", "fabric"),
|
||||
(r"\bmattress\b", "low support surface"),
|
||||
(r"\ba low support surface\b", "a low body support"),
|
||||
(r"\ba low mattress\b", "a low body support"),
|
||||
(r"\ba wide couch\b", "a wide body support"),
|
||||
(r"\bwide couch\b", "wide body support"),
|
||||
(r"\bcouch\b", "body support"),
|
||||
(r"\bsofa\b", "body support"),
|
||||
(r"\bon the bed\b", "on a body support"),
|
||||
(r"\bon a bed\b", "on a body support"),
|
||||
(r"\bbedroom-floor\b", "floor-level"),
|
||||
(r"\bbedroom floor\b", "floor-level"),
|
||||
)
|
||||
|
||||
|
||||
def _clean_inline(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 sanitize_hardcore_environment_anchors(value: Any) -> str:
|
||||
text = _clean_inline(value)
|
||||
if not text:
|
||||
return ""
|
||||
for pattern, replacement in HARDCORE_ENVIRONMENT_ANCHOR_REPLACEMENTS:
|
||||
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
|
||||
text = re.sub(r"\s+,", ",", text)
|
||||
text = re.sub(r",\s*,", ",", text)
|
||||
text = re.sub(r"\s{2,}", " ", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def sanitize_hardcore_axis_value(value: Any) -> Any:
|
||||
if isinstance(value, list):
|
||||
return [sanitize_hardcore_axis_value(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {str(key): sanitize_hardcore_axis_value(item) for key, item in value.items()}
|
||||
return sanitize_hardcore_environment_anchors(value)
|
||||
|
||||
|
||||
def sanitize_hardcore_axis_values(values: Any) -> dict[str, Any]:
|
||||
if not isinstance(values, dict):
|
||||
return {}
|
||||
return {
|
||||
str(key): sanitize_hardcore_axis_value(value)
|
||||
for key, value in values.items()
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
MAX_SWITCH_INPUTS = 64
|
||||
INDEX_SWITCH_MODES = ["pick_input", "route_output"]
|
||||
INDEX_SWITCH_BASES = ["one_based", "zero_based"]
|
||||
INDEX_SWITCH_MISSING_BEHAVIORS = ["fallback", "none", "clamp", "wrap"]
|
||||
|
||||
|
||||
def normalize_index_base(value: Any) -> str:
|
||||
return value if value in INDEX_SWITCH_BASES else "one_based"
|
||||
|
||||
|
||||
def normalize_missing_behavior(value: Any) -> str:
|
||||
return value if value in INDEX_SWITCH_MISSING_BEHAVIORS else "fallback"
|
||||
|
||||
|
||||
def normalize_mode(value: Any) -> str:
|
||||
return value if value in INDEX_SWITCH_MODES else "pick_input"
|
||||
|
||||
|
||||
def available_input_indices(kwargs: dict[str, Any]) -> list[int]:
|
||||
indices = []
|
||||
for key in kwargs:
|
||||
match = re.match(r"^input_(\d+)$", str(key))
|
||||
if match:
|
||||
indices.append(int(match.group(1)))
|
||||
return sorted(set(indices))
|
||||
|
||||
|
||||
def requested_index(index: Any, index_base: str) -> int:
|
||||
requested = int(index)
|
||||
return requested + 1 if normalize_index_base(index_base) == "zero_based" else requested
|
||||
|
||||
|
||||
def resolved_input_index(requested: int, available: list[int], missing_behavior: str) -> int | None:
|
||||
missing_behavior = normalize_missing_behavior(missing_behavior)
|
||||
if requested in available:
|
||||
return requested
|
||||
if missing_behavior in ("fallback", "none") or not available:
|
||||
return None
|
||||
if missing_behavior == "wrap":
|
||||
return available[(requested - 1) % len(available)]
|
||||
if requested <= available[0]:
|
||||
return available[0]
|
||||
if requested >= available[-1]:
|
||||
return available[-1]
|
||||
lower = [value for value in available if value <= requested]
|
||||
return lower[-1] if lower else available[0]
|
||||
|
||||
|
||||
def input_selection(index: Any, index_base: str, missing_behavior: str, kwargs: dict[str, Any]) -> tuple[int, int | None, list[int]]:
|
||||
requested = requested_index(index, index_base)
|
||||
available = available_input_indices(kwargs)
|
||||
selected = resolved_input_index(requested, available, missing_behavior)
|
||||
return requested, selected, available
|
||||
|
||||
|
||||
def route_selection(index: Any, index_base: str, missing_behavior: str, max_outputs: int = MAX_SWITCH_INPUTS) -> tuple[int, int | None]:
|
||||
requested = requested_index(index, index_base)
|
||||
max_outputs = max(1, int(max_outputs))
|
||||
missing_behavior = normalize_missing_behavior(missing_behavior)
|
||||
if 1 <= requested <= max_outputs:
|
||||
return requested, requested
|
||||
if missing_behavior == "wrap":
|
||||
return requested, ((requested - 1) % max_outputs) + 1
|
||||
if missing_behavior == "clamp":
|
||||
return requested, min(max(requested, 1), max_outputs)
|
||||
return requested, None
|
||||
|
||||
|
||||
def input_status(requested: int, selected: int | None, used_fallback: bool, available: list[int]) -> str:
|
||||
available_text = ",".join(str(index) for index in available) or "none"
|
||||
if used_fallback:
|
||||
return f"requested=input_{requested}; selected=fallback; available={available_text}"
|
||||
if selected is None:
|
||||
return f"requested=input_{requested}; selected=none; available={available_text}"
|
||||
return f"requested=input_{requested}; selected=input_{selected}; available={available_text}"
|
||||
|
||||
|
||||
def route_status(requested: int, selected: int | None, max_outputs: int = MAX_SWITCH_INPUTS) -> str:
|
||||
selected_text = "none" if selected is None else f"output_{selected}"
|
||||
return f"requested=output_{requested}; selected={selected_text}; range=1-{max_outputs}"
|
||||
|
||||
|
||||
def lazy_inputs(index: Any, mode: str, index_base: str, missing_behavior: str, kwargs: dict[str, Any]) -> list[str]:
|
||||
mode = normalize_mode(mode)
|
||||
missing_behavior = normalize_missing_behavior(missing_behavior)
|
||||
if mode == "route_output":
|
||||
return ["route_value"] if "route_value" in kwargs else []
|
||||
requested, selected, _available = input_selection(index, index_base, missing_behavior, kwargs)
|
||||
selected_name = f"input_{selected}" if selected is not None else f"input_{requested}"
|
||||
if selected_name in kwargs:
|
||||
return [selected_name]
|
||||
if missing_behavior == "fallback" and "fallback" in kwargs:
|
||||
return ["fallback"]
|
||||
return []
|
||||
@@ -0,0 +1,141 @@
|
||||
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",
|
||||
"krea2_variant_keys",
|
||||
"krea2_prompt_variant_indices",
|
||||
"restored_prompt_axes",
|
||||
}
|
||||
ACTION_CONTEXT_PRIORITY = (
|
||||
"position",
|
||||
"body_position",
|
||||
"body_arrangement",
|
||||
"arrangement",
|
||||
"angle",
|
||||
"surface",
|
||||
"restored_prompt_details",
|
||||
"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, skip_keys=METADATA_AXIS_KEYS))
|
||||
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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,235 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
DEFAULT_EVAL_LOG_PATH = ROOT / "docs" / "krea2-eval-log.json"
|
||||
VALID_RESULTS = {"accepted", "rejected", "inconclusive"}
|
||||
VALID_DECISIONS = {
|
||||
"generator_patch",
|
||||
"provisional_generator_patch",
|
||||
"prompt_guide_rule",
|
||||
"prompt_only_retry",
|
||||
"needs_more_tests",
|
||||
}
|
||||
|
||||
|
||||
def _path_key(path: str | Path | None = None) -> str:
|
||||
return str(Path(path or DEFAULT_EVAL_LOG_PATH).resolve())
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _load_raw_eval_log(path_key: str) -> dict[str, Any]:
|
||||
with Path(path_key).open("r", encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def clear_cache() -> None:
|
||||
_load_raw_eval_log.cache_clear()
|
||||
|
||||
|
||||
def load_eval_log(path: str | Path | None = None) -> dict[str, Any]:
|
||||
return copy.deepcopy(_load_raw_eval_log(_path_key(path)))
|
||||
|
||||
|
||||
def _text(value: Any) -> str:
|
||||
return value if isinstance(value, str) else ""
|
||||
|
||||
|
||||
def _require_text(errors: list[str], entry: dict[str, Any], key: str, min_len: int) -> None:
|
||||
value = _text(entry.get(key)).strip()
|
||||
if len(value) < min_len:
|
||||
errors.append(f"{key} must be at least {min_len} characters")
|
||||
|
||||
|
||||
def _entry_id_slug(variant_key: str) -> str:
|
||||
value = variant_key.removeprefix("pov_")
|
||||
chars = [char.lower() if char.isalnum() else "-" for char in value]
|
||||
slug = "".join(chars).strip("-")
|
||||
while "--" in slug:
|
||||
slug = slug.replace("--", "-")
|
||||
return slug or "krea2-eval"
|
||||
|
||||
|
||||
def entry_template(
|
||||
variant_key: str,
|
||||
*,
|
||||
seed: int,
|
||||
generator_seed: int | None = None,
|
||||
source: str = "sxcp_eval_mcp",
|
||||
date: str = "",
|
||||
result: str = "inconclusive",
|
||||
decision: str = "needs_more_tests",
|
||||
commit: str = "pending",
|
||||
) -> dict[str, Any]:
|
||||
if not isinstance(seed, int) or isinstance(seed, bool):
|
||||
raise ValueError("seed must be an integer")
|
||||
if generator_seed is not None and (not isinstance(generator_seed, int) or isinstance(generator_seed, bool)):
|
||||
raise ValueError("generator_seed must be an integer")
|
||||
variant = _text(variant_key).strip()
|
||||
if not variant:
|
||||
raise ValueError("variant_key is required")
|
||||
entry = {
|
||||
"id": f"{_entry_id_slug(variant)}-{seed}-eval",
|
||||
"date": date,
|
||||
"variant_key": variant,
|
||||
"seed": seed,
|
||||
"source": source,
|
||||
"result": result,
|
||||
"decision": decision,
|
||||
"baseline_prompt_summary": f"Replace this with what the generated {variant} prompt did before the edit.",
|
||||
"candidate_prompt_summary": f"Replace this with what the same-seed candidate prompt changed for {variant}.",
|
||||
"observation": f"Replace this with the fixed-seed Krea2 image comparison observation for {variant}.",
|
||||
"baseline_image": "",
|
||||
"candidate_image": "",
|
||||
"commit": commit,
|
||||
}
|
||||
if generator_seed is not None:
|
||||
entry["generator_seed"] = generator_seed
|
||||
return entry
|
||||
|
||||
|
||||
def validate_entry(
|
||||
entry: dict[str, Any],
|
||||
*,
|
||||
existing_entries: list[dict[str, Any]] | None = None,
|
||||
catalog_keys: set[str] | None = None,
|
||||
) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if not isinstance(entry, dict):
|
||||
return ["entry must be an object"]
|
||||
|
||||
_require_text(errors, entry, "id", 6)
|
||||
entry_id = _text(entry.get("id")).strip()
|
||||
if entry_id and existing_entries:
|
||||
existing_ids = {_text(row.get("id")).strip() for row in existing_entries if isinstance(row, dict)}
|
||||
if entry_id in existing_ids:
|
||||
errors.append(f"duplicate id {entry_id!r}")
|
||||
|
||||
_require_text(errors, entry, "variant_key", 8)
|
||||
variant_key = _text(entry.get("variant_key")).strip()
|
||||
if variant_key and catalog_keys is not None and variant_key not in catalog_keys:
|
||||
errors.append(f"unknown variant {variant_key!r}")
|
||||
|
||||
seed = entry.get("seed")
|
||||
if not isinstance(seed, int) or isinstance(seed, bool):
|
||||
errors.append("seed must be an integer")
|
||||
generator_seed = entry.get("generator_seed")
|
||||
if generator_seed is not None and (not isinstance(generator_seed, int) or isinstance(generator_seed, bool)):
|
||||
errors.append("generator_seed must be an integer")
|
||||
|
||||
result = entry.get("result")
|
||||
if result not in VALID_RESULTS:
|
||||
errors.append(f"result must be one of {sorted(VALID_RESULTS)}")
|
||||
|
||||
decision = entry.get("decision")
|
||||
if decision not in VALID_DECISIONS:
|
||||
errors.append(f"decision must be one of {sorted(VALID_DECISIONS)}")
|
||||
|
||||
_require_text(errors, entry, "baseline_prompt_summary", 20)
|
||||
_require_text(errors, entry, "candidate_prompt_summary", 20)
|
||||
_require_text(errors, entry, "observation", 30)
|
||||
|
||||
for image_key in ("baseline_image", "candidate_image"):
|
||||
image_path = _text(entry.get(image_key)).strip()
|
||||
if not image_path:
|
||||
continue
|
||||
path = Path(image_path)
|
||||
if not path.is_absolute():
|
||||
errors.append(f"{image_key} must be absolute when present")
|
||||
if path.suffix.lower() != ".png":
|
||||
errors.append(f"{image_key} must reference a PNG artifact")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def save_eval_log(log: dict[str, Any], *, path: str | Path | None = None) -> None:
|
||||
target = Path(path or DEFAULT_EVAL_LOG_PATH)
|
||||
target.write_text(json.dumps(log, ensure_ascii=True, indent=2) + "\n", encoding="utf-8")
|
||||
clear_cache()
|
||||
|
||||
|
||||
def append_entry(
|
||||
entry: dict[str, Any],
|
||||
*,
|
||||
path: str | Path | None = None,
|
||||
catalog_path: str | Path | None = None,
|
||||
dry_run: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
from . import krea2_pose_variant_catalog
|
||||
except ImportError: # Allows local smoke tests from the repository root.
|
||||
import krea2_pose_variant_catalog
|
||||
|
||||
log = load_eval_log(path)
|
||||
rows = log.get("entries")
|
||||
if not isinstance(rows, list):
|
||||
rows = []
|
||||
log["entries"] = rows
|
||||
new_entry = copy.deepcopy(entry)
|
||||
errors = validate_entry(
|
||||
new_entry,
|
||||
existing_entries=[row for row in rows if isinstance(row, dict)],
|
||||
catalog_keys=set(krea2_pose_variant_catalog.variant_keys(path=catalog_path)),
|
||||
)
|
||||
if errors:
|
||||
raise ValueError("; ".join(errors))
|
||||
rows.append(new_entry)
|
||||
if not dry_run:
|
||||
save_eval_log(log, path=path)
|
||||
return copy.deepcopy(log)
|
||||
|
||||
|
||||
def entries(
|
||||
*,
|
||||
variant_key: str | None = None,
|
||||
result: str | None = None,
|
||||
decision: str | None = None,
|
||||
path: str | Path | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
log = load_eval_log(path)
|
||||
rows = log.get("entries") or []
|
||||
if not isinstance(rows, list):
|
||||
return []
|
||||
filtered: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
if variant_key is not None and row.get("variant_key") != variant_key:
|
||||
continue
|
||||
if result is not None and row.get("result") != result:
|
||||
continue
|
||||
if decision is not None and row.get("decision") != decision:
|
||||
continue
|
||||
filtered.append(row)
|
||||
return filtered
|
||||
|
||||
|
||||
def entries_for_variant(
|
||||
variant_key: str,
|
||||
*,
|
||||
result: str | None = None,
|
||||
decision: str | None = None,
|
||||
path: str | Path | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
return entries(variant_key=variant_key, result=result, decision=decision, path=path)
|
||||
|
||||
|
||||
def variant_keys(
|
||||
*,
|
||||
result: str | None = None,
|
||||
decision: str | None = None,
|
||||
path: str | Path | None = None,
|
||||
) -> list[str]:
|
||||
keys: list[str] = []
|
||||
for row in entries(result=result, decision=decision, path=path):
|
||||
key = row.get("variant_key")
|
||||
if key and key not in keys:
|
||||
keys.append(str(key))
|
||||
return keys
|
||||
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
DEFAULT_CATALOG_PATH = ROOT / "categories" / "krea2_pov_pose_variants.json"
|
||||
|
||||
|
||||
def _path_key(path: str | Path | None = None) -> str:
|
||||
return str(Path(path or DEFAULT_CATALOG_PATH).resolve())
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _load_raw_catalog(path_key: str) -> dict[str, Any]:
|
||||
with Path(path_key).open("r", encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def clear_cache() -> None:
|
||||
_load_raw_catalog.cache_clear()
|
||||
|
||||
|
||||
def load_catalog(path: str | Path | None = None) -> dict[str, Any]:
|
||||
return copy.deepcopy(_load_raw_catalog(_path_key(path)))
|
||||
|
||||
|
||||
def variants(
|
||||
*,
|
||||
status: str | None = None,
|
||||
family: str | None = None,
|
||||
action_family: str | None = None,
|
||||
path: str | Path | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
catalog = load_catalog(path)
|
||||
rows = catalog.get("variants") or []
|
||||
if not isinstance(rows, list):
|
||||
return []
|
||||
filtered: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
if status is not None and row.get("status") != status:
|
||||
continue
|
||||
if family is not None and row.get("family") != family:
|
||||
continue
|
||||
if action_family is not None and row.get("action_family") != action_family:
|
||||
continue
|
||||
filtered.append(row)
|
||||
return filtered
|
||||
|
||||
|
||||
def variant_keys(
|
||||
*,
|
||||
status: str | None = None,
|
||||
family: str | None = None,
|
||||
action_family: str | None = None,
|
||||
path: str | Path | None = None,
|
||||
) -> list[str]:
|
||||
return [
|
||||
str(row.get("key"))
|
||||
for row in variants(status=status, family=family, action_family=action_family, path=path)
|
||||
if row.get("key")
|
||||
]
|
||||
|
||||
|
||||
def get_variant(key: str, *, path: str | Path | None = None) -> dict[str, Any]:
|
||||
for row in variants(path=path):
|
||||
if row.get("key") == key:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
def _cue_list(value: Any) -> list[str]:
|
||||
if isinstance(value, dict):
|
||||
value = value.get("prompt_cues") or value.get("cues")
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
return [str(cue) for cue in value if str(cue).strip()]
|
||||
|
||||
|
||||
def prompt_cue_sets(variant_or_key: dict[str, Any] | str) -> list[list[str]]:
|
||||
variant = get_variant(variant_or_key) if isinstance(variant_or_key, str) else dict(variant_or_key or {})
|
||||
if not variant:
|
||||
return []
|
||||
cue_sets: list[list[str]] = []
|
||||
baseline = _cue_list(variant.get("prompt_cues"))
|
||||
if baseline:
|
||||
cue_sets.append(baseline)
|
||||
for cue_set in variant.get("prompt_variant_cues") or []:
|
||||
cues = _cue_list(cue_set)
|
||||
if cues:
|
||||
cue_sets.append(cues)
|
||||
if not cue_sets:
|
||||
fallback = str(variant.get("canonical_geometry") or "").strip()
|
||||
if fallback:
|
||||
cue_sets.append([fallback])
|
||||
return cue_sets
|
||||
|
||||
|
||||
def reference_paths(key: str, *, path: str | Path | None = None) -> list[Path]:
|
||||
catalog = load_catalog(path)
|
||||
atlas_root = Path(str(catalog.get("atlas_root") or ""))
|
||||
variant = get_variant(key, path=path)
|
||||
refs = variant.get("reference_images") or []
|
||||
if not isinstance(refs, list):
|
||||
return []
|
||||
paths: list[Path] = []
|
||||
for ref in refs:
|
||||
ref_path = Path(str(ref))
|
||||
if ".." in ref_path.parts:
|
||||
continue
|
||||
paths.append(atlas_root / ref_path)
|
||||
return paths
|
||||
@@ -0,0 +1,426 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from . import krea2_eval_log, krea2_pose_variant_catalog
|
||||
except ImportError: # Allows local smoke tests from the repository root.
|
||||
import krea2_eval_log
|
||||
import krea2_pose_variant_catalog
|
||||
|
||||
|
||||
def _coverage_state(status: str, accepted_count: int) -> str:
|
||||
if status == "proven" and accepted_count > 0:
|
||||
return "proven_with_evidence"
|
||||
if status == "proven":
|
||||
return "proven_missing_evidence"
|
||||
if status == "candidate" and accepted_count == 0:
|
||||
return "needs_fixed_seed_tests"
|
||||
if status == "unstable":
|
||||
return "needs_stronger_control"
|
||||
return "tracked"
|
||||
|
||||
|
||||
def _latest_evidence(entries: list[dict[str, Any]], *, result: str | None = None) -> dict[str, Any]:
|
||||
filtered = [entry for entry in entries if result is None or entry.get("result") == result]
|
||||
if not filtered:
|
||||
return {}
|
||||
entry = filtered[-1]
|
||||
return {
|
||||
"id": entry.get("id") or "",
|
||||
"seed": entry.get("seed"),
|
||||
"generator_seed": entry.get("generator_seed"),
|
||||
"result": entry.get("result") or "",
|
||||
"decision": entry.get("decision") or "",
|
||||
"baseline_prompt_summary": entry.get("baseline_prompt_summary") or "",
|
||||
"candidate_prompt_summary": entry.get("candidate_prompt_summary") or "",
|
||||
"observation": entry.get("observation") or "",
|
||||
"needs_expansion": bool(entry.get("needs_expansion")),
|
||||
"commit": entry.get("commit") or "",
|
||||
}
|
||||
|
||||
|
||||
def coverage_rows() -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
for variant in krea2_pose_variant_catalog.variants():
|
||||
key = str(variant.get("key") or "")
|
||||
evidence = krea2_eval_log.entries_for_variant(key)
|
||||
accepted = [entry for entry in evidence if entry.get("result") == "accepted"]
|
||||
status = str(variant.get("status") or "")
|
||||
rows.append(
|
||||
{
|
||||
"key": key,
|
||||
"family": variant.get("family") or "",
|
||||
"action_family": variant.get("action_family") or "",
|
||||
"status": status,
|
||||
"difficulty": variant.get("difficulty") or "",
|
||||
"priority": variant.get("priority") or "",
|
||||
"control_requirement": variant.get("control_requirement") or "",
|
||||
"coverage_state": _coverage_state(status, len(accepted)),
|
||||
"accepted_evidence_count": len(accepted),
|
||||
"total_evidence_count": len(evidence),
|
||||
"latest_evidence": _latest_evidence(evidence),
|
||||
"latest_accepted_evidence": _latest_evidence(evidence, result="accepted"),
|
||||
"reference_count": len(variant.get("reference_images") or []),
|
||||
"guide_section": (variant.get("evidence") or {}).get("guide_section", ""),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def coverage_summary() -> dict[str, Any]:
|
||||
rows = coverage_rows()
|
||||
status_counts = Counter(row.get("status") for row in rows)
|
||||
state_counts = Counter(row.get("coverage_state") for row in rows)
|
||||
return {
|
||||
"variant_count": len(rows),
|
||||
"status_counts": dict(status_counts),
|
||||
"coverage_state_counts": dict(state_counts),
|
||||
"variants_without_accepted_evidence": [
|
||||
str(row.get("key"))
|
||||
for row in rows
|
||||
if int(row.get("accepted_evidence_count") or 0) == 0
|
||||
],
|
||||
"next_test_candidates": [
|
||||
str(row.get("key"))
|
||||
for row in rows
|
||||
if row.get("coverage_state") in {"needs_fixed_seed_tests", "proven_missing_evidence"}
|
||||
],
|
||||
"stronger_control_cases": [
|
||||
str(row.get("key"))
|
||||
for row in rows
|
||||
if row.get("coverage_state") == "needs_stronger_control"
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _catalog_atlas_root() -> Path:
|
||||
catalog = krea2_pose_variant_catalog.load_catalog()
|
||||
return Path(str(catalog.get("atlas_root") or ""))
|
||||
|
||||
|
||||
def _mapped_atlas_folders() -> dict[str, list[str]]:
|
||||
mapped: dict[str, list[str]] = {}
|
||||
for variant in krea2_pose_variant_catalog.variants():
|
||||
key = str(variant.get("key") or "")
|
||||
for folder in variant.get("atlas_folders") or []:
|
||||
folder_name = str(folder)
|
||||
if not folder_name:
|
||||
continue
|
||||
mapped.setdefault(folder_name, []).append(key)
|
||||
return mapped
|
||||
|
||||
|
||||
def _is_background_or_control_folder(folder_name: str) -> bool:
|
||||
lower = folder_name.lower()
|
||||
return (
|
||||
lower == "bg"
|
||||
or lower == "woman"
|
||||
or lower.endswith("_control")
|
||||
or lower.endswith("_bg")
|
||||
or lower.endswith("_control_bg")
|
||||
)
|
||||
|
||||
|
||||
def _sample_pngs(folder: Path, limit: int) -> list[str]:
|
||||
if not folder.is_dir() or limit <= 0:
|
||||
return []
|
||||
return [str(path) for path in sorted(folder.glob("*.png"), key=lambda path: path.name.lower())[:limit]]
|
||||
|
||||
|
||||
def atlas_folder_rows(atlas_root: str | Path | None = None) -> list[dict[str, Any]]:
|
||||
root = Path(atlas_root) if atlas_root is not None else _catalog_atlas_root()
|
||||
if not root.is_dir():
|
||||
return []
|
||||
mapped = _mapped_atlas_folders()
|
||||
rows: list[dict[str, Any]] = []
|
||||
for folder in sorted(root.iterdir(), key=lambda path: path.name.lower()):
|
||||
if not folder.is_dir():
|
||||
continue
|
||||
folder_name = folder.name
|
||||
if _is_background_or_control_folder(folder_name):
|
||||
continue
|
||||
image_count = sum(1 for _ in folder.glob("*.png"))
|
||||
if image_count <= 0:
|
||||
continue
|
||||
control_folder = root / f"{folder_name}_control"
|
||||
variant_keys = mapped.get(folder_name, [])
|
||||
if not variant_keys and not control_folder.is_dir():
|
||||
continue
|
||||
rows.append(
|
||||
{
|
||||
"folder": folder_name,
|
||||
"image_count": image_count,
|
||||
"mapped": bool(variant_keys),
|
||||
"variant_keys": list(variant_keys),
|
||||
"control_folder": str(control_folder) if control_folder.is_dir() else "",
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def atlas_coverage_summary(atlas_root: str | Path | None = None) -> dict[str, Any]:
|
||||
rows = atlas_folder_rows(atlas_root=atlas_root)
|
||||
unmapped = [str(row.get("folder")) for row in rows if not row.get("mapped")]
|
||||
return {
|
||||
"pose_folder_count": len(rows),
|
||||
"mapped_folder_count": len(rows) - len(unmapped),
|
||||
"unmapped_folder_count": len(unmapped),
|
||||
"unmapped_folders": unmapped,
|
||||
}
|
||||
|
||||
|
||||
def _suggested_variant_key(folder_name: str) -> str:
|
||||
if folder_name.lower() == "ready":
|
||||
return "pov_ejaculation_aftermath_open_thigh_candidate"
|
||||
normalized = "".join(char if char.isalnum() else "_" for char in folder_name.lower()).strip("_")
|
||||
while "__" in normalized:
|
||||
normalized = normalized.replace("__", "_")
|
||||
return f"pov_{normalized}_candidate" if normalized else "pov_unmapped_candidate"
|
||||
|
||||
|
||||
def atlas_gap_plans(atlas_root: str | Path | None = None, sample_limit: int = 3) -> list[dict[str, Any]]:
|
||||
root = Path(atlas_root) if atlas_root is not None else _catalog_atlas_root()
|
||||
plans: list[dict[str, Any]] = []
|
||||
for row in atlas_folder_rows(atlas_root=root):
|
||||
if row.get("mapped"):
|
||||
continue
|
||||
folder_name = str(row.get("folder") or "")
|
||||
folder_path = root / folder_name
|
||||
control_folder = Path(str(row.get("control_folder") or ""))
|
||||
plans.append(
|
||||
{
|
||||
"folder": folder_name,
|
||||
"suggested_variant_key": _suggested_variant_key(folder_name),
|
||||
"image_count": int(row.get("image_count") or 0),
|
||||
"sample_images": _sample_pngs(folder_path, sample_limit),
|
||||
"control_images": _sample_pngs(control_folder, sample_limit),
|
||||
}
|
||||
)
|
||||
return plans
|
||||
|
||||
|
||||
def next_test_plans() -> list[dict[str, Any]]:
|
||||
rows_by_key = {str(row.get("key")): row for row in coverage_rows()}
|
||||
plans: list[dict[str, Any]] = []
|
||||
for key in coverage_summary()["next_test_candidates"]:
|
||||
variant = krea2_pose_variant_catalog.get_variant(key)
|
||||
if not variant:
|
||||
continue
|
||||
row = rows_by_key.get(key, {})
|
||||
evidence = variant.get("evidence") or {}
|
||||
plans.append(
|
||||
{
|
||||
"key": key,
|
||||
"family": variant.get("family") or "",
|
||||
"action_family": variant.get("action_family") or "",
|
||||
"status": variant.get("status") or "",
|
||||
"coverage_state": row.get("coverage_state") or "",
|
||||
"canonical_geometry": variant.get("canonical_geometry") or "",
|
||||
"prompt_cues": list(variant.get("prompt_cues") or []),
|
||||
"avoid_cues": list(variant.get("avoid_cues") or []),
|
||||
"reference_paths": [str(path) for path in krea2_pose_variant_catalog.reference_paths(key)],
|
||||
"generator_hook": variant.get("generator_hook") or {},
|
||||
"guide_section": evidence.get("guide_section") or "",
|
||||
"notes": evidence.get("notes") or "",
|
||||
}
|
||||
)
|
||||
return plans
|
||||
|
||||
|
||||
def guide_expansion_plans() -> list[dict[str, Any]]:
|
||||
plans: list[dict[str, Any]] = []
|
||||
for row in coverage_rows():
|
||||
latest_accepted = row.get("latest_accepted_evidence") or {}
|
||||
decision = str(latest_accepted.get("decision") or "")
|
||||
if decision not in {"prompt_guide_rule", "needs_more_tests"} and not (
|
||||
decision == "provisional_generator_patch" and latest_accepted.get("needs_expansion")
|
||||
):
|
||||
continue
|
||||
key = str(row.get("key") or "")
|
||||
variant = krea2_pose_variant_catalog.get_variant(key)
|
||||
if not variant:
|
||||
continue
|
||||
evidence = variant.get("evidence") or {}
|
||||
plans.append(
|
||||
{
|
||||
"key": key,
|
||||
"family": variant.get("family") or "",
|
||||
"action_family": variant.get("action_family") or "",
|
||||
"status": variant.get("status") or "",
|
||||
"coverage_state": row.get("coverage_state") or "",
|
||||
"target": "multi_seed_multi_woman_matrix",
|
||||
"latest_accepted_id": latest_accepted.get("id") or "",
|
||||
"latest_accepted_seed": latest_accepted.get("seed"),
|
||||
"latest_accepted_decision": decision,
|
||||
"accepted_evidence_count": row.get("accepted_evidence_count") or 0,
|
||||
"total_evidence_count": row.get("total_evidence_count") or 0,
|
||||
"canonical_geometry": variant.get("canonical_geometry") or "",
|
||||
"prompt_cues": list(variant.get("prompt_cues") or []),
|
||||
"avoid_cues": list(variant.get("avoid_cues") or []),
|
||||
"reference_paths": [str(path) for path in krea2_pose_variant_catalog.reference_paths(key)],
|
||||
"generator_hook": variant.get("generator_hook") or {},
|
||||
"guide_section": evidence.get("guide_section") or "",
|
||||
"notes": evidence.get("notes") or "",
|
||||
}
|
||||
)
|
||||
return plans
|
||||
|
||||
|
||||
def next_eval_template_commands(*, seed_token: str = "<fixed_seed>") -> list[dict[str, str]]:
|
||||
commands: list[dict[str, str]] = []
|
||||
for plan in next_test_plans():
|
||||
key = str(plan.get("key") or "")
|
||||
if not key:
|
||||
continue
|
||||
commands.append(
|
||||
{
|
||||
"key": key,
|
||||
"command": f"python tools/krea2_record_eval.py --print-template --variant-key {key} --seed {seed_token}",
|
||||
}
|
||||
)
|
||||
return commands
|
||||
|
||||
|
||||
def markdown_report(atlas_root: str | Path | None = None) -> str:
|
||||
lines = [
|
||||
"# Krea2 Pose Variant Coverage",
|
||||
"",
|
||||
"| Variant | Status | Evidence | State |",
|
||||
"| --- | --- | ---: | --- |",
|
||||
]
|
||||
for row in coverage_rows():
|
||||
lines.append(
|
||||
f"| {row['key']} | {row['status']} | {row['accepted_evidence_count']}/{row['total_evidence_count']} | {row['coverage_state']} |"
|
||||
)
|
||||
evidence_rows = [row for row in coverage_rows() if row.get("latest_evidence")]
|
||||
if evidence_rows:
|
||||
lines.extend(["", "## Latest Evidence", ""])
|
||||
for row in evidence_rows:
|
||||
evidence = row.get("latest_evidence") or {}
|
||||
seed = evidence.get("seed")
|
||||
seed_text = f"seed {seed}" if isinstance(seed, int) else "seed unknown"
|
||||
generator_seed = evidence.get("generator_seed")
|
||||
generator_seed_text = f", generator seed {generator_seed}" if isinstance(generator_seed, int) else ""
|
||||
commit = evidence.get("commit") or "uncommitted"
|
||||
lines.append(
|
||||
f"- {row['key']}: {evidence.get('id') or 'unnamed'} ({evidence.get('result') or 'unknown'}, {seed_text}{generator_seed_text}, {evidence.get('decision') or 'unknown'}, commit {commit})"
|
||||
)
|
||||
if evidence.get("candidate_prompt_summary"):
|
||||
lines.append(f" Candidate: {evidence['candidate_prompt_summary']}")
|
||||
if evidence.get("observation"):
|
||||
lines.append(f" Observation: {evidence['observation']}")
|
||||
accepted = row.get("latest_accepted_evidence") or {}
|
||||
if accepted and accepted.get("id") != evidence.get("id"):
|
||||
accepted_seed = accepted.get("seed")
|
||||
accepted_seed_text = f"seed {accepted_seed}" if isinstance(accepted_seed, int) else "seed unknown"
|
||||
accepted_generator_seed = accepted.get("generator_seed")
|
||||
accepted_generator_seed_text = (
|
||||
f", generator seed {accepted_generator_seed}" if isinstance(accepted_generator_seed, int) else ""
|
||||
)
|
||||
accepted_commit = accepted.get("commit") or "uncommitted"
|
||||
lines.append(
|
||||
f" Latest accepted: {accepted.get('id') or 'unnamed'} ({accepted.get('result') or 'unknown'}, {accepted_seed_text}{accepted_generator_seed_text}, {accepted.get('decision') or 'unknown'}, commit {accepted_commit})"
|
||||
)
|
||||
if accepted.get("candidate_prompt_summary"):
|
||||
lines.append(f" Accepted candidate: {accepted['candidate_prompt_summary']}")
|
||||
if accepted.get("observation"):
|
||||
lines.append(f" Accepted observation: {accepted['observation']}")
|
||||
summary = coverage_summary()
|
||||
if summary["next_test_candidates"]:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Next Fixed-Seed Tests",
|
||||
"",
|
||||
*[f"- {key}" for key in summary["next_test_candidates"]],
|
||||
]
|
||||
)
|
||||
template_commands = next_eval_template_commands()
|
||||
if template_commands:
|
||||
lines.extend(["", "## Eval Entry Template Commands", ""])
|
||||
for command in template_commands:
|
||||
lines.append(f"- {command['key']}: `{command['command']}`")
|
||||
stronger_control_rows = [row for row in coverage_rows() if row.get("coverage_state") == "needs_stronger_control"]
|
||||
if stronger_control_rows:
|
||||
lines.extend(["", "## Stronger Control Cases", ""])
|
||||
for row in stronger_control_rows:
|
||||
difficulty = row.get("difficulty") or "unrated"
|
||||
priority = row.get("priority") or "unprioritized"
|
||||
control_requirement = row.get("control_requirement") or "control_needed"
|
||||
lines.append(
|
||||
f"- {row['key']}: {difficulty}, {priority} priority, {control_requirement}"
|
||||
)
|
||||
expansion_plans = guide_expansion_plans()
|
||||
if expansion_plans:
|
||||
lines.extend(["", "## Guide/Fragile Evidence Expansion", ""])
|
||||
for plan in expansion_plans:
|
||||
seed = plan.get("latest_accepted_seed")
|
||||
seed_text = f"seed {seed}" if isinstance(seed, int) else "seed unknown"
|
||||
lines.append(
|
||||
f"- {plan['key']}: {plan['target']} after {plan['latest_accepted_decision']} "
|
||||
f"({plan['latest_accepted_id']}, {seed_text})"
|
||||
)
|
||||
plans = next_test_plans()
|
||||
if plans:
|
||||
lines.extend(["", "## Next Test Plans"])
|
||||
for plan in plans:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
f"### {plan['key']}",
|
||||
"",
|
||||
f"- Geometry: {plan['canonical_geometry']}",
|
||||
f"- References: {', '.join(plan['reference_paths']) or 'none'}",
|
||||
"- Prompt cues:",
|
||||
*[f" - {cue}" for cue in plan["prompt_cues"]],
|
||||
"- Avoid cues:",
|
||||
*[f" - {cue}" for cue in plan["avoid_cues"]],
|
||||
]
|
||||
)
|
||||
atlas_summary = atlas_coverage_summary(atlas_root=atlas_root)
|
||||
if atlas_summary["pose_folder_count"]:
|
||||
unmapped = atlas_summary["unmapped_folders"]
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Atlas Folder Coverage",
|
||||
"",
|
||||
f"- Pose folders: {atlas_summary['pose_folder_count']}",
|
||||
f"- Mapped folders: {atlas_summary['mapped_folder_count']}",
|
||||
f"- Unmapped folders: {atlas_summary['unmapped_folder_count']}",
|
||||
]
|
||||
)
|
||||
if unmapped:
|
||||
lines.extend(["", "Unmapped atlas folders:", *[f"- {folder}" for folder in unmapped]])
|
||||
gap_plans = atlas_gap_plans(atlas_root=atlas_root)
|
||||
if gap_plans:
|
||||
lines.extend(["", "## Atlas Gap Plans"])
|
||||
for plan in gap_plans:
|
||||
sample_images = plan["sample_images"]
|
||||
control_images = plan["control_images"]
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
f"### {plan['folder']}",
|
||||
"",
|
||||
f"- Suggested key: {plan['suggested_variant_key']}",
|
||||
f"- Pose images: {plan['image_count']}",
|
||||
f"- Samples: {', '.join(sample_images) or 'none'}",
|
||||
f"- Controls: {', '.join(control_images) or 'none'}",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
_ = argv
|
||||
print(markdown_report())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,191 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from .krea_action_context import axis_values_text
|
||||
from .krea_action_positions import action_position_phrase, mentions_rear_entry
|
||||
from .krea_detail import detail_clauses, join_detail_clauses, limit_detail_for_density
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
from krea_action_context import axis_values_text
|
||||
from krea_action_positions import action_position_phrase, mentions_rear_entry
|
||||
from krea_detail import detail_clauses, join_detail_clauses, limit_detail_for_density
|
||||
|
||||
|
||||
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_climax_view_clause(clause: str, role_graph: str) -> str:
|
||||
lower = clause.lower()
|
||||
if "view" not in lower and "frame" not in lower:
|
||||
return clause
|
||||
angle_match = re.search(
|
||||
r"\b(front-facing|close-up|wide full-body|wide|overhead|mirror-reflected|low-angle|side-profile|bed-level)\b",
|
||||
lower,
|
||||
)
|
||||
if not angle_match:
|
||||
return clause
|
||||
angle = angle_match.group(1)
|
||||
if angle == "wide":
|
||||
angle = "wide full-body"
|
||||
position = action_position_phrase(role_graph)
|
||||
if position:
|
||||
return f"{angle} aftermath view with the {position} readable"
|
||||
return f"{angle} aftermath view"
|
||||
|
||||
|
||||
def climax_clause_duplicates_role(clause: str, role_graph: str) -> bool:
|
||||
clause_lower = clause.lower()
|
||||
role_lower = role_graph.lower()
|
||||
role_has_ejaculation = any(token in role_lower for token in ("ejaculates semen", "visible semen", "semen lands"))
|
||||
if role_has_ejaculation and re.search(
|
||||
r"\b(?:cum clearly visible|explicit semen aftermath visible|hardcore ejaculation detail visible|"
|
||||
r"post-ejaculation fluids anatomically clear|sexual fluids and body contact visible|"
|
||||
r"visible external ejaculation|hardcore ejaculation scene|visible orgasm aftermath)\b",
|
||||
clause_lower,
|
||||
):
|
||||
return True
|
||||
duplicate_pairs = (
|
||||
(("lower back", "ass"), ("lower back", "ass")),
|
||||
(("ass",), ("ass",)),
|
||||
(("pussy", "thigh"), ("pussy", "thigh")),
|
||||
(("face", "lips"), ("face", "lips")),
|
||||
(("tongue", "chin"), ("face", "lips", "mouth", "tongue")),
|
||||
(("breast",), ("breast", "chest")),
|
||||
(("belly",), ("belly", "torso")),
|
||||
(("body",), ("body",)),
|
||||
)
|
||||
if any(token in clause_lower for token in ("cum", "semen", "fluid")):
|
||||
for clause_tokens, role_tokens in duplicate_pairs:
|
||||
if any(token in clause_lower for token in clause_tokens) and any(token in role_lower for token in role_tokens):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None) -> str:
|
||||
role_graph = _clean(role_graph).rstrip(".")
|
||||
text = " ".join(part.lower() for part in (role_graph, _clean(hard_item), axis_values_text(axis_values)) if part)
|
||||
if "the woman" not in text or "the man" not in text:
|
||||
return role_graph
|
||||
if "lying between two partners" in text or "lies between" in text:
|
||||
return "the woman lies between two partners, the man under her hips and another partner over her torso as visible semen lands on her body"
|
||||
if "held between front-and-back partners" in text:
|
||||
return "the woman is held between the man behind her and another partner in front of her as visible semen lands across her body"
|
||||
if "kneeling between standing partners" in text:
|
||||
return "the woman kneels between standing partners gathered around her face and torso for visible ejaculation"
|
||||
if "side-lying with thighs parted" in text:
|
||||
return "the woman lies on her side with thighs parted while the man kneels beside her hips and ejaculates semen across her thighs and pussy"
|
||||
if "sitting on the edge of the bed" in text:
|
||||
return "the woman sits on the edge of the bed with knees spread while the man stands close between her legs and ejaculates semen across her body"
|
||||
if "lying at the bed edge with thighs open" in text:
|
||||
return "the woman lies at the bed edge with thighs open while the man kneels between her legs and ejaculates semen across her pussy and thighs"
|
||||
if "reclining with thighs open" in text or "lying on the back with legs spread" in text:
|
||||
return (
|
||||
"the woman lies on her back with thighs open after ejaculation; thick semen and clear fluid cover her exposed pussy "
|
||||
"and inner thighs as the exact-center aftermath detail, her body stays still, and her face and torso remain visible behind the open thighs"
|
||||
)
|
||||
if "on all fours with hips raised" in text:
|
||||
return "the woman is on all fours with hips raised while the man is positioned behind her and ejaculates semen across her ass, thighs, and lower back"
|
||||
if "face-down ass-up" in text or "lies face-down" in text or "face down" in text:
|
||||
return "the woman lies face-down with ass raised while the man is positioned behind her and ejaculates semen across her lower back and ass"
|
||||
if "bent over with ass raised" in text or "bent over" in text:
|
||||
return "the woman bends forward with hips raised while the man stands behind her with visible semen across her lower back, ass, and thighs"
|
||||
if "kneeling with mouth open" in text:
|
||||
return "the woman kneels in front of the man at hip height as he ejaculates semen onto her face, lips, and chest"
|
||||
if "kneeling in front of a standing partner" in text:
|
||||
return "the woman kneels in front of the man at hip height while he stands over her and ejaculates semen across her body"
|
||||
if "standing with cum on the body" in text:
|
||||
return "the woman stands braced in front of the man while he stands close at hip level and ejaculates semen across her body"
|
||||
if "squatting on top of a partner" in text:
|
||||
return "the woman squats over the man's hips while the man lies on his back under her and ejaculates semen onto her body"
|
||||
if "reverse cowgirl over a partner's hips" in text:
|
||||
return "the woman straddles the man's hips facing away while the man lies on his back under her and ejaculates semen onto her body"
|
||||
if "straddles" in text or "straddling a partner" in text or "straddling a partner's hips" in text or "shared climax after penetration" in text:
|
||||
return "the woman straddles the man's hips while the man lies on his back under her and ejaculates semen onto her body"
|
||||
if "seated in a partner's lap facing them" in text:
|
||||
return "the woman sits in the man's lap facing him, legs wrapped around his hips as he ejaculates semen across her body"
|
||||
if "lower back" in text or "cum dripping from ass" in text or "cum on lower back" in text or mentions_rear_entry(text):
|
||||
return "the woman bends forward with hips raised while the man stands behind her with visible semen across her lower back, ass, and thighs"
|
||||
if "cum on face" in text or "cum on tongue" in text or "cum on lips" in text or "cum on tongue and chin" in text:
|
||||
return "the woman kneels in front of the man at hip height as he ejaculates semen onto her face, lips, and chest"
|
||||
if (
|
||||
"cum dripping from pussy" in text
|
||||
or "arousal dripping from pussy" in text
|
||||
or "open thighs" in text
|
||||
):
|
||||
return (
|
||||
"the woman lies on her back with thighs open after ejaculation; thick semen and clear fluid cover her exposed pussy "
|
||||
"and inner thighs as the exact-center aftermath detail, her body stays still, and her face and torso remain visible behind the open thighs"
|
||||
)
|
||||
if role_graph:
|
||||
return role_graph
|
||||
return "the woman lies on her back with thighs open while the man kneels between her legs and ejaculates semen across her body"
|
||||
|
||||
|
||||
def dedupe_climax_detail(detail: str, role_graph: str, density: str = "balanced") -> str:
|
||||
detail = _clean(detail)
|
||||
lower = role_graph.lower()
|
||||
patterns: list[str] = []
|
||||
if "solo visible ejaculation" in lower or "one hand on his penis" in lower:
|
||||
detail = re.sub(r"\bcum on lower back and ass\b", "visible semen on skin", detail, flags=re.IGNORECASE)
|
||||
detail = re.sub(r"\bcum (?:on|dripping from) ass\b", "visible semen on skin", detail, flags=re.IGNORECASE)
|
||||
if "lies on her back" in lower:
|
||||
patterns.extend((r"lying on the back with legs spread and hips lifted", r"reclining with thighs open", r"lying on the back with legs spread"))
|
||||
detail = re.sub(r"\bcum on lower back and ass\b", "cum across thighs and pussy", detail, flags=re.IGNORECASE)
|
||||
detail = re.sub(r"\bcum (?:on|dripping from) ass\b", "cum across thighs and pussy", detail, flags=re.IGNORECASE)
|
||||
if "straddles" in lower:
|
||||
patterns.extend(
|
||||
(
|
||||
r"straddling a partner's hips in cowgirl position",
|
||||
r"reverse cowgirl over a partner's hips",
|
||||
r"straddling a partner",
|
||||
r"squatting on top of a partner",
|
||||
)
|
||||
)
|
||||
if "squats over" in lower:
|
||||
patterns.append(r"squatting on top of a partner")
|
||||
if "sits in the man's lap" in lower:
|
||||
patterns.append(r"seated in a partner's lap facing them")
|
||||
if "bends forward" in lower:
|
||||
patterns.append(r"bent over with ass raised")
|
||||
if "on all fours" in lower:
|
||||
patterns.append(r"on all fours with hips raised")
|
||||
if "face-down" in lower:
|
||||
patterns.append(r"face-down ass-up on the mattress")
|
||||
if "lies on her side" in lower:
|
||||
patterns.append(r"side-lying with thighs parted")
|
||||
detail = re.sub(r"\bcum on lower back and ass\b", "cum across thighs and pussy", detail, flags=re.IGNORECASE)
|
||||
detail = re.sub(r"\bcum (?:on|dripping from) ass\b", "cum across thighs and pussy", detail, flags=re.IGNORECASE)
|
||||
if "sits on the edge" in lower:
|
||||
patterns.append(r"sitting on the edge of the bed")
|
||||
if "bed edge" in lower:
|
||||
patterns.append(r"lying at the bed edge with thighs open")
|
||||
if "kneels in front" in lower:
|
||||
patterns.extend((r"kneeling with mouth open", r"kneeling in front of a standing partner"))
|
||||
if "stands braced" in lower:
|
||||
patterns.append(r"standing with cum on the body")
|
||||
for pattern in patterns:
|
||||
detail = re.sub(rf"\b{pattern}\b,?\s*", "", detail, flags=re.IGNORECASE)
|
||||
if not any(token in lower for token in ("face", "mouth", "lips", "tongue")):
|
||||
detail = re.sub(r"\bsaliva and cum mixed on the mouth\b", "visible semen on skin", detail, flags=re.IGNORECASE)
|
||||
detail = re.sub(r"\bcum on tongue and chin\b", "visible semen on skin", detail, flags=re.IGNORECASE)
|
||||
detail = re.sub(r"\bcum on face and lips\b", "visible semen on skin", detail, flags=re.IGNORECASE)
|
||||
detail = re.sub(r",\s*,", ",", detail)
|
||||
detail = re.sub(r"\bwith\s*,\s*", "with ", detail, flags=re.IGNORECASE)
|
||||
detail = re.sub(r"^with\s+", "", detail, flags=re.IGNORECASE)
|
||||
detail = re.sub(r"^and\s+", "", detail, flags=re.IGNORECASE)
|
||||
clauses: list[str] = []
|
||||
for clause in detail_clauses(detail):
|
||||
normalized = normalize_climax_view_clause(clause, role_graph)
|
||||
if climax_clause_duplicates_role(normalized, role_graph):
|
||||
continue
|
||||
if density != "dense" and normalized.lower() in ("orgasm during penetration", "post-orgasm visible release"):
|
||||
continue
|
||||
clauses.append(normalized)
|
||||
return limit_detail_for_density(join_detail_clauses(clauses), density, True)
|
||||
@@ -0,0 +1,286 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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"}
|
||||
|
||||
|
||||
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_hardcore_detail_density(value: Any) -> str:
|
||||
text = _clean(value).lower()
|
||||
return text if text in HARDCORE_DETAIL_DENSITY_CHOICES else "balanced"
|
||||
|
||||
|
||||
def axis_values_text(axis_values: Any) -> str:
|
||||
return item_axis_policy.action_context_text(axis_values)
|
||||
|
||||
|
||||
def position_context_text(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
|
||||
return " ".join(
|
||||
_clean(part).lower()
|
||||
for part in (role_graph, hard_item, composition, axis_values_text(axis_values))
|
||||
if _clean(part)
|
||||
)
|
||||
|
||||
|
||||
def is_outercourse_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"outercourse",
|
||||
"non-penetrative",
|
||||
"boobjob",
|
||||
"titjob",
|
||||
"breast sex",
|
||||
"breast-sex",
|
||||
"testicle",
|
||||
"balls",
|
||||
"balls licking",
|
||||
"balls-licking",
|
||||
"breasts tightly around",
|
||||
"breasts around",
|
||||
"penis licking",
|
||||
"penis-licking",
|
||||
"tongue along",
|
||||
"tongue runs along",
|
||||
"tongue running along",
|
||||
"handjob",
|
||||
"hand job",
|
||||
"hand wrapped",
|
||||
"hand stroking",
|
||||
"hand wraps around",
|
||||
"manual stimulation",
|
||||
"fingering",
|
||||
"fingers inside",
|
||||
"fingers in pussy",
|
||||
"hand on pussy",
|
||||
"fingers on pussy",
|
||||
"fingers sliding against the pussy",
|
||||
"open-thigh manual",
|
||||
"clit rubbing",
|
||||
"clit",
|
||||
"clitoris",
|
||||
"mutual masturbation",
|
||||
"footjob",
|
||||
"soles wrap around",
|
||||
"soles",
|
||||
"toes curled",
|
||||
"feet stroking",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_oral_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"oral",
|
||||
"fellatio",
|
||||
"blowjob",
|
||||
"deepthroat",
|
||||
"penis sucking",
|
||||
"penis in her mouth",
|
||||
"penis in mouth",
|
||||
"takes the man's penis",
|
||||
"takes his penis",
|
||||
"mouth at penis level",
|
||||
"mouth on his penis",
|
||||
"lips wrapped",
|
||||
"cunnilingus",
|
||||
"pussy licking",
|
||||
"mouth on her pussy",
|
||||
"mouth pressed to her pussy",
|
||||
"face-sitting",
|
||||
"sixty-nine",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_foreplay_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
if not text:
|
||||
return False
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"foreplay",
|
||||
"pre-sex",
|
||||
"before sex",
|
||||
"before penetration",
|
||||
"kissing",
|
||||
"deep kiss",
|
||||
"mouth-to-mouth",
|
||||
"lips pressed",
|
||||
"caressing",
|
||||
"hands roaming",
|
||||
"stroking skin",
|
||||
"touching breasts",
|
||||
"cupping a breast",
|
||||
"hand on the cheek",
|
||||
"cheek and jaw",
|
||||
"fingers under the chin",
|
||||
"undressing",
|
||||
"removing clothing",
|
||||
"removing clothes",
|
||||
"pulling clothing",
|
||||
"sliding straps",
|
||||
"unbuttoning",
|
||||
"body worship",
|
||||
"nipple",
|
||||
"mouth on skin",
|
||||
"kissing down",
|
||||
"ass grabbing",
|
||||
"gripping the ass",
|
||||
"thigh kissing",
|
||||
"inner thighs",
|
||||
"hair held",
|
||||
"holding hair",
|
||||
"hair pulled back",
|
||||
"wrist",
|
||||
"wrists",
|
||||
"pinning",
|
||||
"guided",
|
||||
"guiding",
|
||||
"turning the body",
|
||||
"position transition",
|
||||
"pulling onto the bed",
|
||||
"lifting and spreading",
|
||||
"spreading thighs",
|
||||
"dirty talk",
|
||||
"whispering",
|
||||
"camera performance",
|
||||
"presented directly to the camera",
|
||||
"present her body",
|
||||
"showing to camera",
|
||||
"spread open for the camera",
|
||||
"watching partner",
|
||||
"waiting turn",
|
||||
"group coordination",
|
||||
"aftercare",
|
||||
"cleanup",
|
||||
"wiping",
|
||||
"towel",
|
||||
"post-sex",
|
||||
"fingering",
|
||||
"fingers inside",
|
||||
"hand on pussy",
|
||||
"fingers on pussy",
|
||||
"clit rubbing",
|
||||
"clit",
|
||||
"clitoris",
|
||||
"manual stimulation",
|
||||
"mutual masturbation",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_close_foreplay_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
if not text or not is_foreplay_text(text):
|
||||
return False
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"stand close",
|
||||
"stand face-to-face",
|
||||
"press their bodies",
|
||||
"bodies pressed close",
|
||||
"hips pressed close",
|
||||
"mouth-to-mouth",
|
||||
"deep kissing",
|
||||
"heated kiss",
|
||||
"hands pull clothing",
|
||||
"pull clothing aside",
|
||||
"clothing being removed",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_vaginal_penetration_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
if not text or is_outercourse_text(text) or is_oral_text(text) or is_foreplay_text(text):
|
||||
return False
|
||||
if any(term in text for term in ("anal", "double penetration", "double-penetration", "toy-assisted", "strap-on")):
|
||||
return False
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"vaginal penetration",
|
||||
"deep vaginal sex",
|
||||
"explicit penetrative sex",
|
||||
"penetrative sex",
|
||||
"penis entering pussy",
|
||||
"penis thrusts into her pussy",
|
||||
"penis thrusts into the woman",
|
||||
"pussy stretched around a penis",
|
||||
"hardcore vaginal thrusting",
|
||||
"full-body penetrative sex",
|
||||
"close-contact vaginal sex",
|
||||
"missionary position",
|
||||
"cowgirl position",
|
||||
"reverse cowgirl position",
|
||||
"doggy style position",
|
||||
"standing sex position",
|
||||
"spooning sex position",
|
||||
"edge-of-bed position",
|
||||
"kneeling straddle position",
|
||||
"lotus sex position",
|
||||
"bent-over position",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_toy_assisted_double_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
return any(
|
||||
token in text
|
||||
for token in (
|
||||
"double penetration",
|
||||
"double-penetration",
|
||||
"front-and-back double",
|
||||
"vaginal and anal penetration",
|
||||
"pussy and ass filled",
|
||||
"one penis in pussy and one penis in ass",
|
||||
"second penetration point",
|
||||
"second point of contact",
|
||||
"second contact",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_climax_text(*parts: str) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
return any(
|
||||
token in text
|
||||
for token in (
|
||||
"cumshot",
|
||||
"ejaculation",
|
||||
"post-orgasm",
|
||||
"post-climax",
|
||||
"orgasm aftermath",
|
||||
"orgasm scene",
|
||||
"orgasm during",
|
||||
"shared climax",
|
||||
"hardcore climax",
|
||||
"external cumshot",
|
||||
"visible external ejaculation",
|
||||
"climaxes on",
|
||||
"climax lands",
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,472 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from . import outercourse_action_policy as outercourse_policy
|
||||
from .krea_action_context import (
|
||||
is_close_foreplay_text,
|
||||
position_context_text,
|
||||
)
|
||||
from .krea_detail import detail_clauses, join_detail_clauses
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
import outercourse_action_policy as outercourse_policy
|
||||
from krea_action_context import (
|
||||
is_close_foreplay_text,
|
||||
position_context_text,
|
||||
)
|
||||
from krea_detail import detail_clauses, join_detail_clauses
|
||||
|
||||
|
||||
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 strip_redundant_position_detail(detail: str) -> str:
|
||||
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:
|
||||
return ""
|
||||
if not is_close_foreplay_text(role_graph, detail, composition):
|
||||
return detail
|
||||
detail = re.sub(
|
||||
r"\b(?:raised edge|edge-supported|edge-of-bed|bed-edge)\s+undressing position\s+(?:featuring|while|with)\s+",
|
||||
"",
|
||||
detail,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
detail = re.sub(
|
||||
r"\b(?:standing kissing|wall-pressed kissing|mirror undressing)\s+position\s+(?:featuring|while|with)\s+",
|
||||
"",
|
||||
detail,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
detail = re.sub(
|
||||
r"\b(?:raised edge|edge-supported|edge-of-bed|bed-edge)\s+undressing position\b",
|
||||
"close standing undressing",
|
||||
detail,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
detail = re.sub(r"\braised-edge open-thigh position\b", "close-body first-person position", detail, flags=re.IGNORECASE)
|
||||
detail = re.sub(r"\s*,\s*", ", ", detail).strip(" ,;")
|
||||
return _clean(detail)
|
||||
|
||||
|
||||
def hardcore_item_detail(hard_item: str) -> str:
|
||||
text = _clean(hard_item).rstrip(".")
|
||||
if not text:
|
||||
return ""
|
||||
text = re.sub(r"^hardcore\s+", "", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r"^explicit\s+", "", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r"^(?:orgasm|climax)\s+scene:\s*", "", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r"^(?:mouth-to-genitals|double-contact sex|adult group pile|sex pile)\s+pose:\s*", "", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r"^(?:oral|threesome|orgy)\s+scene\s+with\s+", "", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r"^(?:threesome|orgy)\s+pose:\s*", "", text, flags=re.IGNORECASE)
|
||||
act_patterns = (
|
||||
r"(?:penis and toy|toy and strap-on|toy-assisted|front-and-back|hardcore|deep|kneeling|standing supported)?\s*double penetration",
|
||||
r"toy-assisted vaginal and anal penetration at the same time",
|
||||
r"vaginal and anal penetration at the same time",
|
||||
r"one penis in pussy and one penis in ass",
|
||||
r"anal penetration with visible genital contact",
|
||||
r"rear-entry anal penetration",
|
||||
r"anal sex with spread cheeks",
|
||||
r"ass stretched around a penis",
|
||||
r"penis entering ass",
|
||||
r"deep anal sex",
|
||||
r"bent-over anal sex",
|
||||
r"hardcore anal thrusting",
|
||||
r"vaginal penetration with visible genital contact",
|
||||
r"penis entering pussy",
|
||||
r"pussy stretched around a penis",
|
||||
r"deep vaginal sex",
|
||||
r"explicit penetrative sex",
|
||||
r"penetrative sex",
|
||||
r"hardcore vaginal thrusting",
|
||||
r"full-body penetrative sex",
|
||||
r"close-contact vaginal sex",
|
||||
r"fellatio with penis in mouth",
|
||||
r"deepthroat blowjob",
|
||||
r"blowjob",
|
||||
r"penis sucking with visible saliva",
|
||||
r"cunnilingus with tongue on pussy",
|
||||
r"face-sitting cunnilingus",
|
||||
r"pussy licking with thighs spread",
|
||||
r"oral sex with tongue and fingers",
|
||||
r"oral contact with mouth on the visible genitals",
|
||||
r"sixty-nine oral sex",
|
||||
)
|
||||
act_pattern = "|".join(act_patterns)
|
||||
position_pattern = (
|
||||
r"missionary position|cowgirl position|reverse cowgirl position|doggy style position|"
|
||||
r"standing sex position|spooning sex position|edge-of-bed position|kneeling straddle position|"
|
||||
r"lotus sex position|bent-over position|kneeling oral position|face-sitting position|"
|
||||
r"sixty-nine position|edge-of-bed oral position|edge-supported oral position|standing oral position|reclining cunnilingus position|"
|
||||
r"straddled oral position|side-lying oral position|spread-leg oral position|chair oral position"
|
||||
)
|
||||
text = re.sub(
|
||||
rf"^({position_pattern})\s+(?:while|with|featuring)\s+(?:{act_pattern})\s*,?\s*",
|
||||
r"\1, ",
|
||||
text,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
text = re.sub(
|
||||
rf"^(?:{act_pattern})\s*(?:in|from|on|with|while|featuring)?\s*",
|
||||
"",
|
||||
text,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
text = re.sub(r"^(?:position|pose)\s+", "", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r"^with\s+", "", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r"\bwith with\b", "with", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r",\s*with\s+", ", ", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r",\s+and\s+", ", ", text)
|
||||
text = re.sub(r"\s*,\s*", ", ", text).strip(" ,;")
|
||||
return _clean(text)
|
||||
|
||||
|
||||
def dedupe_anchor_detail(detail: str, anchor: str) -> str:
|
||||
detail = strip_redundant_position_detail(detail)
|
||||
anchor_lower = anchor.lower()
|
||||
duplicate_phrases = {
|
||||
"front-and-back": (r"front-and-back contact",),
|
||||
"side-lying oral": (r"side-lying oral position",),
|
||||
"kneeling oral": (r"kneeling oral position",),
|
||||
"face-sitting": (r"face-sitting position",),
|
||||
"sixty-nine": (
|
||||
r"sixty-nine position",
|
||||
r"sixty-nine oral sex",
|
||||
r"kneeling oral position",
|
||||
r"face-sitting position",
|
||||
r"edge-of-bed oral position",
|
||||
r"standing oral position",
|
||||
r"reclining cunnilingus position",
|
||||
r"straddled oral position",
|
||||
r"side-lying oral position",
|
||||
r"spread-leg oral position",
|
||||
r"chair oral position",
|
||||
),
|
||||
"edge-supported oral": (r"edge-of-bed oral position", r"edge-supported oral position"),
|
||||
"edge-of-bed oral": (r"edge-of-bed oral position", r"edge-supported oral position"),
|
||||
"standing oral": (r"standing oral position",),
|
||||
"spread-leg oral": (r"spread-leg oral position",),
|
||||
"chair oral": (r"chair oral position",),
|
||||
"reclining cunnilingus": (r"reclining cunnilingus position",),
|
||||
"straddled cunnilingus": (r"straddled oral position", r"straddled cunnilingus position"),
|
||||
"open-thigh cunnilingus": (r"reclining cunnilingus position", r"straddled cunnilingus position"),
|
||||
"bent-over": (r"bent-over position",),
|
||||
"face-down": (r"face-down ass-up position",),
|
||||
"missionary": (r"missionary position",),
|
||||
"reverse cowgirl": (r"reverse cowgirl position",),
|
||||
"cowgirl": (r"cowgirl position",),
|
||||
"doggy-style": (r"doggy style position",),
|
||||
"edge-supported": (r"edge-of-bed position", r"edge-supported position", r"raised edge position"),
|
||||
"edge-of-bed": (r"edge-of-bed position", r"edge-supported position"),
|
||||
"lotus": (r"lotus sex position",),
|
||||
"standing sex": (r"standing sex position",),
|
||||
"spooning": (r"spooning sex position", r"spooning anal position"),
|
||||
}
|
||||
for anchor_token, phrases in duplicate_phrases.items():
|
||||
if anchor_token in anchor_lower:
|
||||
for phrase in phrases:
|
||||
detail = re.sub(rf"\b{phrase}\b,?\s*", "", detail, flags=re.IGNORECASE)
|
||||
detail = re.sub(r"^\s*,\s*", "", detail)
|
||||
detail = re.sub(r",\s*,", ",", detail)
|
||||
return _clean(detail).strip(" ,;")
|
||||
|
||||
|
||||
def dedupe_toy_double_detail(detail: str) -> str:
|
||||
detail = _clean(detail)
|
||||
if not detail:
|
||||
return ""
|
||||
angle_view = (
|
||||
r"(?:rear-view|side-profile|low-angle|mirror-reflected|overhead|close-up|wide full-body|front-facing with hips turned)"
|
||||
)
|
||||
toy_act = (
|
||||
r"(?:penis and toy double penetration|toy-assisted vaginal and anal penetration at the same time|toy and strap-on double penetration)"
|
||||
)
|
||||
detail = re.sub(
|
||||
rf"\b({angle_view}\s+view of\s+){toy_act}\b",
|
||||
r"\1the rear-entry contact",
|
||||
detail,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
detail = re.sub(rf",?\s*\b{toy_act}\b", "", detail, flags=re.IGNORECASE)
|
||||
duplicate_phrases = (
|
||||
"toy-assisted second contact aligned behind the body",
|
||||
"toy aligned for a second penetration point",
|
||||
"rear-entry body alignment",
|
||||
"close body alignment",
|
||||
"stacked bodies in close contact",
|
||||
"one body between two partners",
|
||||
"one partner behind and one partner in front",
|
||||
"two partners penetrating at once",
|
||||
"one partner held between two bodies",
|
||||
"front-and-back contact",
|
||||
"three bodies locked together",
|
||||
"kneeling center partner",
|
||||
)
|
||||
for phrase in duplicate_phrases:
|
||||
detail = re.sub(rf",?\s*\b{re.escape(phrase)}\b", "", detail, flags=re.IGNORECASE)
|
||||
detail = re.sub(r"^\s*,\s*", "", detail)
|
||||
detail = re.sub(r",\s*,", ",", detail)
|
||||
return _clean(detail).strip(" ,;")
|
||||
|
||||
|
||||
def dedupe_outercourse_detail(detail: str, role_graph: str, hard_item: str = "", axis_values: Any = None) -> str:
|
||||
detail = strip_redundant_position_detail(detail)
|
||||
if not detail:
|
||||
return ""
|
||||
context = position_context_text(role_graph, hard_item, "", axis_values)
|
||||
context_lower = context.lower()
|
||||
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] = []
|
||||
for clause in detail_clauses(detail):
|
||||
lower = clause.lower()
|
||||
if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
|
||||
if lower in ("penis", "breasts", "mouth clearly visible"):
|
||||
continue
|
||||
if any(
|
||||
term in lower
|
||||
for term in (
|
||||
"boobjob",
|
||||
"titjob",
|
||||
"breast-sex",
|
||||
"breast sex",
|
||||
"seated titjob position",
|
||||
"kneeling boobjob position",
|
||||
"tight close-up breast-sex position",
|
||||
"penis shaft compressed between breasts",
|
||||
"penis squeezed between both breasts",
|
||||
"hands pressing the breasts tightly",
|
||||
"hands pressing breasts firmly together",
|
||||
"fingers spreading the breasts around the penis shaft",
|
||||
"soft flesh squeezed around the penis shaft",
|
||||
"hand wrapped around the penis shaft",
|
||||
"glans near the mouth",
|
||||
"glans visible",
|
||||
"penis, breasts, and mouth clearly visible",
|
||||
)
|
||||
):
|
||||
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)
|
||||
return join_detail_clauses(clauses)
|
||||
|
||||
|
||||
def dedupe_oral_detail(detail: str, role_graph: str, hard_item: str = "", axis_values: Any = None) -> str:
|
||||
detail = _clean(detail)
|
||||
if not detail:
|
||||
return ""
|
||||
context = position_context_text(role_graph, hard_item, "", axis_values)
|
||||
woman_gives = any(
|
||||
term in context
|
||||
for term in (
|
||||
"takes the man's penis",
|
||||
"takes his penis",
|
||||
"penis in her mouth",
|
||||
"mouth at penis level",
|
||||
"mouth on his penis",
|
||||
"fellatio",
|
||||
"blowjob",
|
||||
"deepthroat",
|
||||
"penis sucking",
|
||||
)
|
||||
)
|
||||
clauses: list[str] = []
|
||||
for clause in detail_clauses(detail):
|
||||
lower = clause.lower()
|
||||
if any(
|
||||
term in lower
|
||||
for term in (
|
||||
"kneeling oral position",
|
||||
"standing oral position",
|
||||
"edge-of-bed oral position",
|
||||
"side-lying oral position",
|
||||
"chair oral position",
|
||||
"reclining cunnilingus position",
|
||||
"face-sitting position",
|
||||
"sixty-nine position",
|
||||
"fellatio with penis in mouth",
|
||||
"deepthroat blowjob",
|
||||
"penis sucking with visible saliva",
|
||||
"cunnilingus with tongue on pussy",
|
||||
"oral sex with tongue and fingers",
|
||||
"oral contact with mouth on the visible genitals",
|
||||
"bodies stacked close together",
|
||||
"body angle keeps the penis and face readable",
|
||||
)
|
||||
):
|
||||
continue
|
||||
if woman_gives and lower == "wet shine on genitals":
|
||||
clause = "saliva dripping on the penis"
|
||||
clauses.append(clause)
|
||||
return join_detail_clauses(clauses)
|
||||
|
||||
|
||||
def dedupe_penetration_detail(detail: str, role_graph: str, hard_item: str = "", axis_values: Any = None) -> str:
|
||||
detail = _clean(detail)
|
||||
if not detail:
|
||||
return ""
|
||||
role_lower = _clean(role_graph).lower()
|
||||
detail = re.sub(
|
||||
r"\b(?:front-facing|side-profile|rear-view|overhead|mirror-reflected|low-angle|close-up|wide full-body)\s+view of\s+"
|
||||
r"(?:vaginal penetration with visible genital contact|deep vaginal sex|explicit penetrative sex|penetrative sex|"
|
||||
r"penis entering pussy|pussy stretched around a penis|hardcore vaginal thrusting|full-body penetrative sex|"
|
||||
r"close-contact vaginal sex)\b,?\s*",
|
||||
"",
|
||||
detail,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
act_terms = (
|
||||
"vaginal penetration with visible genital contact",
|
||||
"deep vaginal sex",
|
||||
"explicit penetrative sex",
|
||||
"penetrative sex",
|
||||
"penis entering pussy",
|
||||
"pussy stretched around a penis",
|
||||
"hardcore vaginal thrusting",
|
||||
"full-body penetrative sex",
|
||||
"close-contact vaginal sex",
|
||||
"missionary position",
|
||||
"cowgirl position",
|
||||
"reverse cowgirl position",
|
||||
"doggy style position",
|
||||
"standing sex position",
|
||||
"spooning sex position",
|
||||
"edge-of-bed position",
|
||||
"kneeling straddle position",
|
||||
"lotus sex position",
|
||||
"bent-over position",
|
||||
)
|
||||
clauses: list[str] = []
|
||||
for clause in detail_clauses(detail):
|
||||
lower = clause.lower()
|
||||
if any(term in lower for term in act_terms):
|
||||
continue
|
||||
if lower in (
|
||||
"tongues visible while kissing",
|
||||
"deep kissing",
|
||||
"mouth close to the ear",
|
||||
"neck kissing",
|
||||
"explicit genital contact visible",
|
||||
"genitals clearly visible",
|
||||
"anatomically clear penetration",
|
||||
"pussy and penis visible",
|
||||
"wetness visible between the thighs",
|
||||
):
|
||||
continue
|
||||
if lower in ("legs spread wide", "thighs open toward the viewer") and any(
|
||||
term in role_lower for term in ("legs spread wide", "thighs open", "open thighs")
|
||||
):
|
||||
continue
|
||||
if lower == "one body pinned under another" and "lies under" in role_lower:
|
||||
continue
|
||||
if lower in ("hips locked tightly together", "hips aligned") and "hips" in role_lower:
|
||||
continue
|
||||
if lower in ("hands gripping hips", "hands spreading the thighs") and any(
|
||||
term in role_lower for term in ("hips", "thighs", "legs")
|
||||
):
|
||||
continue
|
||||
clauses.append(clause)
|
||||
return join_detail_clauses(clauses)
|
||||
@@ -0,0 +1,213 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from .krea_action_context import (
|
||||
axis_values_text,
|
||||
is_climax_text,
|
||||
is_toy_assisted_double_text,
|
||||
normalize_hardcore_detail_density,
|
||||
)
|
||||
from .hardcore_action_metadata import (
|
||||
ACTION_ANAL,
|
||||
ACTION_CLIMAX,
|
||||
ACTION_FOREPLAY,
|
||||
ACTION_MANUAL,
|
||||
ACTION_ORAL,
|
||||
ACTION_OUTERCOURSE,
|
||||
ACTION_PENETRATION,
|
||||
ACTION_TOY_DOUBLE,
|
||||
infer_hardcore_action_family,
|
||||
normalize_hardcore_action_family,
|
||||
)
|
||||
from .krea_detail import limit_detail_for_density
|
||||
from .krea_action_positions import hardcore_pose_anchor
|
||||
from .krea_action_details import (
|
||||
dedupe_anchor_detail,
|
||||
dedupe_oral_detail,
|
||||
dedupe_outercourse_detail,
|
||||
dedupe_penetration_detail,
|
||||
dedupe_toy_double_detail,
|
||||
hardcore_item_detail,
|
||||
sanitize_foreplay_detail,
|
||||
)
|
||||
from .krea_action_climax import climax_role_graph, dedupe_climax_detail
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
from krea_action_context import (
|
||||
axis_values_text,
|
||||
is_climax_text,
|
||||
is_toy_assisted_double_text,
|
||||
normalize_hardcore_detail_density,
|
||||
)
|
||||
from hardcore_action_metadata import (
|
||||
ACTION_ANAL,
|
||||
ACTION_CLIMAX,
|
||||
ACTION_FOREPLAY,
|
||||
ACTION_MANUAL,
|
||||
ACTION_ORAL,
|
||||
ACTION_OUTERCOURSE,
|
||||
ACTION_PENETRATION,
|
||||
ACTION_TOY_DOUBLE,
|
||||
infer_hardcore_action_family,
|
||||
normalize_hardcore_action_family,
|
||||
)
|
||||
from krea_detail import limit_detail_for_density
|
||||
from krea_action_positions import hardcore_pose_anchor
|
||||
from krea_action_details import (
|
||||
dedupe_anchor_detail,
|
||||
dedupe_oral_detail,
|
||||
dedupe_outercourse_detail,
|
||||
dedupe_penetration_detail,
|
||||
dedupe_toy_double_detail,
|
||||
hardcore_item_detail,
|
||||
sanitize_foreplay_detail,
|
||||
)
|
||||
from krea_action_climax import climax_role_graph, dedupe_climax_detail
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HardcoreActionParts:
|
||||
family: str
|
||||
role_graph: str
|
||||
hard_item: str
|
||||
detail: str
|
||||
anchor: str
|
||||
detail_density: str
|
||||
|
||||
|
||||
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_hardcore_role_graph(role_graph: str) -> str:
|
||||
role_graph = _clean(role_graph).rstrip(".")
|
||||
replacements = (
|
||||
(
|
||||
r"\bthe man penetrates the woman while a toy adds a second point of contact\b",
|
||||
"the man's penis thrusts into the woman while a toy is positioned at the second penetration point",
|
||||
),
|
||||
(
|
||||
r"\bthe man thrusts his penis into the woman while a toy adds a second penetration point\b",
|
||||
"the man's penis thrusts into the woman while a toy is positioned at the second penetration point",
|
||||
),
|
||||
(
|
||||
r"\bthe man thrusts his penis into the woman\b",
|
||||
"the man's penis thrusts into the woman",
|
||||
),
|
||||
(
|
||||
r"\bthe man penetrates the woman anally\b",
|
||||
"the man's penis thrusts into the woman's ass",
|
||||
),
|
||||
(
|
||||
r"\bthe man thrusts his penis into the woman's ass\b",
|
||||
"the man's penis thrusts into the woman's ass",
|
||||
),
|
||||
(
|
||||
r"\bthe man penetrates the woman\b",
|
||||
"the man's penis thrusts into the woman",
|
||||
),
|
||||
(
|
||||
r"\bthe woman and the man are in mutual oral contact with mouth-to-genital contact visible\b",
|
||||
"the woman has the man's penis in her mouth while the man uses his mouth on her pussy",
|
||||
),
|
||||
(
|
||||
r"\bthe woman gives oral to the man\b",
|
||||
"the woman takes the man's penis in her mouth",
|
||||
),
|
||||
)
|
||||
for pattern, replacement in replacements:
|
||||
role_graph = re.sub(pattern, replacement, role_graph, flags=re.IGNORECASE)
|
||||
return role_graph
|
||||
|
||||
|
||||
def normalize_toy_double_role_graph(role_graph: str) -> str:
|
||||
return re.sub(
|
||||
r"\s+while a toy adds (?:the|a) second penetration point\b",
|
||||
" while a toy is positioned at the second penetration point",
|
||||
role_graph,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def action_detail_for_family(
|
||||
family: str,
|
||||
detail: str,
|
||||
role_graph: str,
|
||||
hard_item: str,
|
||||
composition: str = "",
|
||||
axis_values: Any = None,
|
||||
*,
|
||||
anchor: str = "",
|
||||
detail_density: str = "balanced",
|
||||
) -> tuple[str, str]:
|
||||
if family == ACTION_CLIMAX:
|
||||
return "", dedupe_climax_detail(detail, role_graph, detail_density)
|
||||
if family in (ACTION_FOREPLAY, ACTION_MANUAL):
|
||||
detail = sanitize_foreplay_detail(detail, role_graph, composition)
|
||||
return "", limit_detail_for_density(detail, detail_density, False)
|
||||
if family == ACTION_OUTERCOURSE:
|
||||
detail = dedupe_outercourse_detail(detail, role_graph, hard_item, axis_values)
|
||||
return "", limit_detail_for_density(detail, detail_density, False)
|
||||
if family == ACTION_ORAL and role_graph:
|
||||
detail = dedupe_oral_detail(detail, role_graph, hard_item, axis_values)
|
||||
return "", limit_detail_for_density(detail, detail_density, False)
|
||||
if family in (ACTION_ANAL, ACTION_PENETRATION) and role_graph:
|
||||
detail = dedupe_penetration_detail(detail, role_graph, hard_item, axis_values)
|
||||
return "", limit_detail_for_density(detail, detail_density, False)
|
||||
|
||||
if anchor:
|
||||
detail = dedupe_anchor_detail(detail, anchor)
|
||||
if family == ACTION_TOY_DOUBLE:
|
||||
detail = dedupe_toy_double_detail(detail)
|
||||
return anchor, limit_detail_for_density(detail, detail_density, False)
|
||||
|
||||
|
||||
def resolve_hardcore_action_parts(
|
||||
role_graph: str,
|
||||
hard_item: str,
|
||||
composition: str = "",
|
||||
axis_values: Any = None,
|
||||
detail_density: str = "balanced",
|
||||
action_family: Any = "",
|
||||
) -> HardcoreActionParts:
|
||||
detail_density = normalize_hardcore_detail_density(detail_density)
|
||||
role_graph = normalize_hardcore_role_graph(role_graph)
|
||||
hard_item = _clean(hard_item).rstrip(".")
|
||||
axis_text = axis_values_text(axis_values)
|
||||
forced_family = normalize_hardcore_action_family(action_family)
|
||||
is_climax = forced_family == ACTION_CLIMAX or is_climax_text(role_graph, hard_item, composition, axis_text)
|
||||
if is_climax:
|
||||
role_graph = climax_role_graph(role_graph, hard_item, axis_values)
|
||||
|
||||
detail = hardcore_item_detail(hard_item)
|
||||
anchor = hardcore_pose_anchor(role_graph, hard_item, composition, axis_values)
|
||||
family = forced_family or infer_hardcore_action_family(role_graph, hard_item, composition, axis_values, is_climax=is_climax)
|
||||
|
||||
if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_text):
|
||||
role_graph = normalize_toy_double_role_graph(role_graph)
|
||||
|
||||
anchor, detail = action_detail_for_family(
|
||||
family,
|
||||
detail,
|
||||
role_graph,
|
||||
hard_item,
|
||||
composition,
|
||||
axis_values,
|
||||
anchor=anchor,
|
||||
detail_density=detail_density,
|
||||
)
|
||||
return HardcoreActionParts(
|
||||
family=family,
|
||||
role_graph=role_graph,
|
||||
hard_item=hard_item,
|
||||
detail=detail,
|
||||
anchor=anchor,
|
||||
detail_density=detail_density,
|
||||
)
|
||||
@@ -0,0 +1,491 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from . import outercourse_action_policy as outercourse_policy
|
||||
from .krea_action_context import (
|
||||
axis_values_text,
|
||||
is_close_foreplay_text,
|
||||
is_foreplay_text,
|
||||
is_outercourse_text,
|
||||
is_toy_assisted_double_text,
|
||||
position_context_text,
|
||||
)
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
import outercourse_action_policy as outercourse_policy
|
||||
from krea_action_context import (
|
||||
axis_values_text,
|
||||
is_close_foreplay_text,
|
||||
is_foreplay_text,
|
||||
is_outercourse_text,
|
||||
is_toy_assisted_double_text,
|
||||
position_context_text,
|
||||
)
|
||||
|
||||
|
||||
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 mentions_rear_entry(text: str) -> bool:
|
||||
return bool(
|
||||
re.search(
|
||||
r"ass[- ](?:up|raised|exposed|lifted|stretched)|penis entering ass|cum (?:on|dripping from) ass|spread cheeks|lower back and ass|pussy, ass|rear[- ]entry",
|
||||
text,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
|
||||
text = position_context_text(role_graph, hard_item, composition, axis_values)
|
||||
item_text = " ".join(part for part in (_clean(hard_item).lower(), axis_values_text(axis_values).lower()) if part)
|
||||
position_text = ""
|
||||
if isinstance(axis_values, dict):
|
||||
position_text = _clean(axis_values.get("position", "")).lower()
|
||||
if not text:
|
||||
return ""
|
||||
if is_foreplay_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
||||
return ""
|
||||
if is_outercourse_text(role_graph, hard_item, composition, axis_values_text(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)
|
||||
if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
|
||||
return "breast-sex outercourse pose"
|
||||
if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
|
||||
return "testicle-sucking outercourse pose"
|
||||
if action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING:
|
||||
return "penis-licking outercourse pose"
|
||||
if action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
|
||||
return "handjob outercourse pose"
|
||||
if action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
|
||||
return "footjob outercourse pose"
|
||||
return "non-penetrative outercourse pose"
|
||||
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:
|
||||
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:
|
||||
return f"{prefix}rear-entry double-penetration pose"
|
||||
if "bent-over" in text or "bent forward" in text:
|
||||
return f"{prefix}bent-over double-penetration pose"
|
||||
if "spooning anal" in text or "side-lying anal" in text or "side-lying" in text:
|
||||
return f"{prefix}side-lying double-penetration pose"
|
||||
if "edge-supported" in text or "bed-edge" in text or "edge-of-bed" in text:
|
||||
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:
|
||||
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:
|
||||
return f"{prefix}kneeling front-and-back double-penetration pose" if front_back else f"{prefix}kneeling 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 "face-down ass-up" in text:
|
||||
return "face-down rear-entry double-penetration pose"
|
||||
if "doggy style" in text or "doggy-style" in text:
|
||||
return "doggy-style double-penetration pose"
|
||||
if "bent-over" in text:
|
||||
return "bent-over double-penetration pose"
|
||||
if "spooning anal" in text or "side-lying anal" in text:
|
||||
return "side-lying double-penetration pose"
|
||||
if "bed-edge" in text or "edge-of-bed" in text:
|
||||
return "bed-edge front-and-back double-penetration pose"
|
||||
if "standing anal" in text or "standing supported" in text:
|
||||
return "standing supported front-and-back double-penetration pose"
|
||||
if "kneeling anal" in text:
|
||||
return "kneeling rear-entry double-penetration pose"
|
||||
if "standing supported" in text:
|
||||
return "standing supported front-and-back double-penetration pose"
|
||||
if "kneeling" in text:
|
||||
return "kneeling front-and-back double-penetration pose"
|
||||
return "front-and-back double-penetration pose"
|
||||
if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text):
|
||||
return "sixty-nine oral pose"
|
||||
if "face-sitting" in position_text or ("face-sitting" in text and not position_text):
|
||||
return "face-sitting oral pose"
|
||||
if "side-lying oral" in position_text or (("side-lying oral position" in item_text or "side-lying oral" in text) and not position_text):
|
||||
return "side-lying oral pose"
|
||||
if (
|
||||
"edge-of-bed oral" in position_text
|
||||
or "edge-supported oral" in position_text
|
||||
or (("edge-of-bed oral position" in item_text or "edge-of-bed oral" in text or "edge-supported oral" in text) and not position_text)
|
||||
):
|
||||
return "edge-supported oral pose"
|
||||
if "standing oral" in position_text or (("standing oral position" in item_text or "standing oral" in text) and not position_text):
|
||||
return "standing oral pose"
|
||||
if "chair oral" in position_text or (("chair oral position" in item_text or "chair oral" in text) and not position_text):
|
||||
return "chair oral pose"
|
||||
if "kneeling oral" in position_text or (("kneeling oral position" in item_text or "kneeling oral" in text) and not position_text):
|
||||
return "kneeling oral pose"
|
||||
if "straddled oral" in position_text or (("straddled oral position" in item_text or "straddled oral" in text) and not position_text):
|
||||
return "straddled cunnilingus pose"
|
||||
if "reclining cunnilingus" in position_text or (("reclining cunnilingus position" in item_text or "reclining cunnilingus" in text) and not position_text):
|
||||
return "reclining cunnilingus pose"
|
||||
if "spread-leg oral" in position_text or (("spread-leg oral position" in item_text or "spread-leg oral" in text) and not position_text):
|
||||
return "spread-leg oral pose"
|
||||
if "cunnilingus" in text or "pussy licking" in text or "mouth on her pussy" in text:
|
||||
if "reclining" in text:
|
||||
return "reclining cunnilingus pose"
|
||||
if "straddled" in text:
|
||||
return "straddled cunnilingus pose"
|
||||
return "open-thigh cunnilingus pose"
|
||||
if "oral" in text or "blowjob" in text or "penis in her mouth" in text or "penis in mouth" in text:
|
||||
if "side-lying oral position" in item_text:
|
||||
return "side-lying oral pose"
|
||||
if "spread-leg oral position" in item_text:
|
||||
return "spread-leg oral pose"
|
||||
if "edge-of-bed oral position" in item_text:
|
||||
return "edge-supported oral pose"
|
||||
if "standing oral position" in item_text:
|
||||
return "standing oral pose"
|
||||
if "chair oral position" in item_text:
|
||||
return "chair oral pose"
|
||||
if "kneeling oral position" in item_text or "kneeling" in text:
|
||||
return "kneeling oral pose"
|
||||
if "standing" in text:
|
||||
return "standing oral pose"
|
||||
if "side-lying" in text:
|
||||
return "side-lying oral pose"
|
||||
if "edge-of-bed" in text or "bed-edge" in text:
|
||||
return "edge-supported oral pose"
|
||||
if "spread-leg" in text:
|
||||
return "spread-leg oral pose"
|
||||
if "chair oral" in text:
|
||||
return "chair oral pose"
|
||||
return "mouth-to-genitals oral pose"
|
||||
if "anal" in text or mentions_rear_entry(text) or "rear-entry" in text:
|
||||
if "face-down ass-up" in text:
|
||||
return "face-down ass-up rear-entry anal pose"
|
||||
if "doggy style" in text or "doggy-style" in text:
|
||||
return "doggy-style anal pose"
|
||||
if "bed-edge" in text or "edge-of-bed" in text:
|
||||
return "bed-edge rear-entry anal pose"
|
||||
if "bent-over" in text:
|
||||
return "bent-over rear-entry anal pose"
|
||||
if "spooning anal" in text or "side-lying anal" in text:
|
||||
return "side-lying rear-entry anal pose"
|
||||
if "kneeling anal" in text:
|
||||
return "kneeling rear-entry anal pose"
|
||||
if "standing anal" in text:
|
||||
return "standing rear-entry anal pose"
|
||||
if "doggy" in text:
|
||||
return "doggy-style anal pose"
|
||||
return "rear-entry anal pose"
|
||||
if "edge-supported" in text or "raised edge" in text or "edge-of-bed" in text or "bed-edge" in text:
|
||||
return "edge-supported penetrative sex pose"
|
||||
if "folded missionary" in text or "knees-to-chest" in text or "knees to chest" in text:
|
||||
return "folded missionary penetrative sex pose"
|
||||
if "cowgirl-alt" in text or "low cowgirl" in text or "seated-squat cowgirl" in text or "low seated squat" in text:
|
||||
return "low cowgirl seated-squat penetrative sex pose"
|
||||
positions = (
|
||||
"missionary",
|
||||
"reverse cowgirl",
|
||||
"cowgirl",
|
||||
"doggy style",
|
||||
"standing sex",
|
||||
"spooning sex",
|
||||
"edge-of-bed",
|
||||
"kneeling straddle",
|
||||
"lotus",
|
||||
"bent-over",
|
||||
)
|
||||
for position in positions:
|
||||
if position in text:
|
||||
return f"{position.replace('doggy style', 'doggy-style')} pose"
|
||||
if "threesome" in text or "three-body" in text:
|
||||
return "three-body explicit sex pose"
|
||||
if "group" in text or "orgy" in text:
|
||||
return "multi-body explicit sex pose"
|
||||
if re.search(r"(?<!non-)penetrat|thrust", text):
|
||||
return "hip-aligned penetrative sex pose"
|
||||
return ""
|
||||
|
||||
|
||||
def hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
|
||||
text = position_context_text(anchor, f"{role_graph} {hard_item}", composition, axis_values)
|
||||
position_text = ""
|
||||
if isinstance(axis_values, dict):
|
||||
position_text = _clean(axis_values.get("position", "")).lower()
|
||||
if not text:
|
||||
return ""
|
||||
mixed_woman_man = "the woman" in text and "the man" in text
|
||||
is_double = "double-penetration" in text or "double penetration" in text
|
||||
|
||||
def cast_phrase(mixed: str, generic: str) -> str:
|
||||
return mixed if mixed_woman_man else generic
|
||||
|
||||
def double_tail() -> str:
|
||||
return "" if "toy" in text else ", with the second penetration point aligned"
|
||||
|
||||
if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text):
|
||||
return cast_phrase(
|
||||
"with the woman and man inverted head-to-hips so both mouths align with genitals",
|
||||
"with both bodies inverted head-to-hips so both mouths align with genitals",
|
||||
)
|
||||
if "face-sitting" in position_text or ("face-sitting" in text and not position_text):
|
||||
return cast_phrase(
|
||||
"with the man lying back while the woman straddles his face",
|
||||
"with one partner lying back while the other straddles the face",
|
||||
)
|
||||
if (
|
||||
"reclining cunnilingus" in position_text
|
||||
or "spread-leg oral" in position_text
|
||||
or (("reclining cunnilingus" in text or "spread-leg oral" in text) and not position_text)
|
||||
):
|
||||
if "takes the man's penis" in text or "penis in her mouth" in text:
|
||||
return cast_phrase(
|
||||
"with the man seated with legs apart and the woman positioned at his hips",
|
||||
"with the receiver seated with legs apart and the giver positioned at the hips",
|
||||
)
|
||||
return cast_phrase(
|
||||
"with the woman lying back, thighs spread, and the man positioned between her legs",
|
||||
"with the receiving partner lying back, thighs spread, and the giver positioned between the legs",
|
||||
)
|
||||
if (
|
||||
"straddled oral" in position_text
|
||||
or (("straddled cunnilingus" in text or "straddled oral" in text) and not position_text)
|
||||
):
|
||||
return cast_phrase(
|
||||
"with the woman straddling above the man's mouth and her thighs framing his face",
|
||||
"with the receiver straddling above the giver's mouth",
|
||||
)
|
||||
if (
|
||||
"edge-of-bed oral" in position_text
|
||||
or "edge-supported oral" in position_text
|
||||
or ("edge-of-bed oral" in text and not position_text)
|
||||
or ("edge-supported oral" in text and not position_text)
|
||||
):
|
||||
if "takes the man's penis" in text or "penis in her mouth" in text:
|
||||
return cast_phrase(
|
||||
"with the man at a raised edge and the woman kneeling at his hips",
|
||||
"with the receiver at a raised edge and the giver positioned at hip height",
|
||||
)
|
||||
return cast_phrase(
|
||||
"with the woman lying at a raised edge and the man positioned between her open thighs",
|
||||
"with the receiver lying at a raised edge and the giver positioned between open thighs",
|
||||
)
|
||||
if "standing oral" in position_text or ("standing oral" in text and not position_text):
|
||||
if "takes the man's penis" in text or "penis in her mouth" in text:
|
||||
return cast_phrase(
|
||||
"with the man standing and the woman kneeling in front of his hips",
|
||||
"with the receiver standing and the giver kneeling at hip height",
|
||||
)
|
||||
return cast_phrase(
|
||||
"with the woman standing braced and the man kneeling between her thighs",
|
||||
"with the receiver standing braced and the giver kneeling between the thighs",
|
||||
)
|
||||
if "chair oral" in position_text or ("chair oral" in text and not position_text):
|
||||
if "takes the man's penis" in text or "penis in her mouth" in text:
|
||||
return cast_phrase(
|
||||
"with the man seated in the chair and the woman kneeling between his legs at hip level",
|
||||
"with the receiver seated in the chair and the giver kneeling between the legs at hip level",
|
||||
)
|
||||
return cast_phrase(
|
||||
"with one partner seated in a chair and the other kneeling between the open thighs",
|
||||
"with the receiver seated in a chair and the giver kneeling between the open thighs",
|
||||
)
|
||||
if "side-lying oral" in position_text or ("side-lying oral" in text and not position_text):
|
||||
return "with both bodies lying on their sides and mouth aligned to genitals"
|
||||
if "kneeling oral" in position_text or ("kneeling oral" in text and not position_text):
|
||||
if "takes the man's penis" in text or "penis in her mouth" in text:
|
||||
return cast_phrase(
|
||||
"with the woman kneeling in front of the man's hips, her mouth at penis level",
|
||||
"with the giver kneeling in front of the receiver's hips",
|
||||
)
|
||||
if "mouth on her pussy" in text or "uses his mouth on" in text:
|
||||
return cast_phrase(
|
||||
"with the man kneeling between the woman's open thighs, his mouth at her pussy",
|
||||
"with the giver kneeling between the receiver's open thighs",
|
||||
)
|
||||
return "with the giver kneeling at the receiver's hips"
|
||||
if "reverse cowgirl alt" in text or "upright reverse cowgirl" in text or "upright back-facing straddle" in text:
|
||||
return cast_phrase(
|
||||
"with the man lying on his back under the woman while she sits upright straddling his hips facing away",
|
||||
"with the lower partner lying on their back while the upper partner sits upright straddling them facing away",
|
||||
)
|
||||
if "reverse cowgirl" in text:
|
||||
return cast_phrase(
|
||||
"with the man lying on his back under the woman while she straddles his hips facing away",
|
||||
"with the lower partner lying on their back while the upper partner straddles them facing away",
|
||||
)
|
||||
if "cowgirl" in text:
|
||||
return cast_phrase(
|
||||
"with the man lying on his back under the woman while she straddles his hips on top",
|
||||
"with the lower partner lying on their back while the upper partner straddles their hips on top",
|
||||
)
|
||||
if "missionary" in text:
|
||||
return cast_phrase(
|
||||
"with the woman lying on her back under the man, legs open around his hips",
|
||||
"with the receiving partner lying on their back under the penetrating partner, legs open around the hips",
|
||||
)
|
||||
if "lotus" in text:
|
||||
return cast_phrase(
|
||||
"with the man seated upright and the woman seated in his lap facing him, legs wrapped around his hips",
|
||||
"with one partner seated upright and the other seated in their lap facing them, legs wrapped around the hips",
|
||||
)
|
||||
if "kneeling straddle" in text:
|
||||
return cast_phrase(
|
||||
"with the woman straddling the man's kneeling lap, both torsos upright and hips pressed together",
|
||||
"with one partner straddling the other's kneeling lap, torsos upright and hips pressed together",
|
||||
)
|
||||
if "doggy-style" in text:
|
||||
return cast_phrase(
|
||||
f"with the woman on all fours and the man positioned behind her at hip level{double_tail() if is_double else ''}",
|
||||
f"with the receiving partner on all fours and the penetrating partner positioned behind at hip level{double_tail() if is_double else ''}",
|
||||
)
|
||||
if "face-down" in text:
|
||||
return cast_phrase(
|
||||
f"with the woman face-down, hips raised, and the man positioned behind her{double_tail() if is_double else ''}",
|
||||
f"with the receiving partner face-down, hips raised, and the penetrating partner positioned behind{double_tail() if is_double else ''}",
|
||||
)
|
||||
if "bent-over" in text:
|
||||
return cast_phrase(
|
||||
f"with the woman bent forward at the waist and the man positioned behind her{double_tail() if is_double else ''}",
|
||||
f"with the receiving partner bent forward at the waist and the penetrating partner positioned behind{double_tail() if is_double else ''}",
|
||||
)
|
||||
if "spooning" in text or ("side-lying" in text and "oral" not in text):
|
||||
return cast_phrase(
|
||||
f"with both lying on their sides and the man positioned behind the woman{double_tail() if is_double else ''}",
|
||||
f"with both bodies lying on their sides and the penetrating partner positioned behind{double_tail() if is_double else ''}",
|
||||
)
|
||||
if "edge-of-bed" in text or "bed-edge" in text:
|
||||
return cast_phrase(
|
||||
f"with the woman lying at the bed edge, hips at the edge, and the man kneeling between her legs{double_tail() if is_double else ''}",
|
||||
f"with the receiver lying at the bed edge, hips at the edge, and the penetrating partner kneeling between the legs{double_tail() if is_double else ''}",
|
||||
)
|
||||
if "standing" in text:
|
||||
return cast_phrase(
|
||||
f"with the woman braced standing and the man aligned at her hips{double_tail() if is_double else ''}",
|
||||
f"with both partners standing and the penetrating partner aligned at the receiver's hips{double_tail() if is_double else ''}",
|
||||
)
|
||||
if "kneeling" in text and ("anal" in text or "rear-entry" in text):
|
||||
return cast_phrase(
|
||||
f"with the woman kneeling forward and the man positioned behind her{double_tail() if is_double else ''}",
|
||||
f"with the receiving partner kneeling forward and the penetrating partner positioned behind{double_tail() if is_double else ''}",
|
||||
)
|
||||
if "double-penetration" in text or "double penetration" in text:
|
||||
if "toy" in text:
|
||||
return cast_phrase(
|
||||
"with the woman on all fours and the man positioned behind her at hip level",
|
||||
"with the receiving body on all fours and the penetrating partner positioned behind at hip level",
|
||||
)
|
||||
if "from the front" in text:
|
||||
return cast_phrase(
|
||||
"with the woman held between the man behind her and a second partner in front",
|
||||
"with the receiving body held between one partner behind and a second partner in front",
|
||||
)
|
||||
return cast_phrase(
|
||||
"with the woman held in a front-and-back position so both contact points are visible",
|
||||
"with the central body held in a front-and-back position so both contact points are visible",
|
||||
)
|
||||
if "anal" in text or mentions_rear_entry(text) or "rear-entry" in text:
|
||||
return cast_phrase(
|
||||
"with the woman's hips raised, ass exposed, and the man positioned behind her",
|
||||
"with the receiving partner's hips raised and the penetrating partner positioned behind",
|
||||
)
|
||||
if "cunnilingus" in text or "mouth on her pussy" in text or "pussy licking" in text:
|
||||
return cast_phrase(
|
||||
"with the woman's thighs open and the man's mouth pressed to her pussy",
|
||||
"with the receiver's thighs open and the giver's mouth pressed to genitals",
|
||||
)
|
||||
if "oral" in text or "blowjob" in text or "penis in her mouth" in text or "penis in mouth" in text:
|
||||
if "takes the man's penis in her mouth" in text or "penis in her mouth" in text:
|
||||
return cast_phrase(
|
||||
"with the woman's mouth at the man's hips",
|
||||
"with the giver's mouth positioned at the receiver's hips",
|
||||
)
|
||||
return "with mouth and genitals aligned clearly"
|
||||
if "threesome" in text or "three-body" in text:
|
||||
return "with all three adult bodies clearly placed around the central subject"
|
||||
if "group" in text or "orgy" in text:
|
||||
return "with each adult body readable in the shared sex act"
|
||||
if re.search(r"(?<!non-)penetrat|thrust", text):
|
||||
return "with hips aligned and legs open around the contact point"
|
||||
return ""
|
||||
|
||||
|
||||
def arrangement_duplicates_role(arrangement: str, role_graph: str) -> bool:
|
||||
arrangement_lower = _clean(arrangement).lower()
|
||||
role_lower = _clean(role_graph).lower()
|
||||
if not arrangement_lower or not role_lower:
|
||||
return False
|
||||
markers = (
|
||||
"bed edge",
|
||||
"on all fours",
|
||||
"face-down",
|
||||
"hips raised",
|
||||
"bent forward",
|
||||
"straddl",
|
||||
"on her back",
|
||||
"on their sides",
|
||||
"on her side",
|
||||
"seated in",
|
||||
"sits in",
|
||||
"lap",
|
||||
"kneeling between",
|
||||
"kneels between",
|
||||
"kneeling in front",
|
||||
"kneels in front",
|
||||
"positioned behind",
|
||||
"standing",
|
||||
)
|
||||
return any(marker in arrangement_lower and marker in role_lower for marker in markers)
|
||||
|
||||
|
||||
def action_position_phrase(action: str) -> str:
|
||||
action = _clean(action).lower()
|
||||
if is_close_foreplay_text(action):
|
||||
return "single-frame close-body first-person position"
|
||||
if "pov upright reverse cowgirl" in action or "pov reverse cowgirl alt" in action:
|
||||
return "upright reverse-cowgirl first-person position"
|
||||
if "pov reverse cowgirl" in action:
|
||||
return "reverse-cowgirl first-person position"
|
||||
if "pov cowgirl" in action:
|
||||
return "cowgirl first-person position"
|
||||
if "pov missionary" in action:
|
||||
return "missionary first-person position"
|
||||
if "pov raised-edge" in action or "raised edge" in action:
|
||||
return "raised-edge open-thigh position"
|
||||
if "pov doggy" in action or "on all fours" in action:
|
||||
return "all-fours rear-entry position"
|
||||
if "pov bent-over" in action or "bent forward" in action:
|
||||
return "bent-over rear-entry position"
|
||||
if "pov face-down" in action:
|
||||
return "face-down rear-entry position"
|
||||
if "pov standing" in action:
|
||||
return "standing rear-entry position"
|
||||
if "pov side-lying" in action:
|
||||
return "side-lying position"
|
||||
if "pov lotus" in action:
|
||||
return "lap-straddling position"
|
||||
if "face-down" in action and "ass raised" in action:
|
||||
return "face-down raised-hip position"
|
||||
if "on all fours" in action:
|
||||
return "all-fours raised-hip position"
|
||||
if "bends forward" in action or "bent forward" in action:
|
||||
return "bent-over raised-hip position"
|
||||
if "lies on her back" in action and ("thighs open" in action or "legs open" in action):
|
||||
return "open-thigh reclined position"
|
||||
if "lies at the bed edge" in action or "bed edge" in action:
|
||||
return "bed-edge position"
|
||||
if "lies on her side" in action:
|
||||
return "side-lying position"
|
||||
if "kneels in front" in action:
|
||||
return "kneeling-at-hip-height position"
|
||||
if "straddles" in action or "squats over" in action:
|
||||
return "straddling position"
|
||||
if "sits in the man's lap" in action:
|
||||
return "lap-straddling position"
|
||||
if "stands braced" in action:
|
||||
return "standing braced position"
|
||||
if "held between" in action or "front-and-back" in action:
|
||||
return "front-and-back position"
|
||||
if "lies between" in action:
|
||||
return "between-partners position"
|
||||
return ""
|
||||
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from .krea_action_positions import (
|
||||
arrangement_duplicates_role,
|
||||
hardcore_pose_arrangement,
|
||||
)
|
||||
from .krea_action_dispatch import resolve_hardcore_action_parts
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
from krea_action_positions import (
|
||||
arrangement_duplicates_role,
|
||||
hardcore_pose_arrangement,
|
||||
)
|
||||
from krea_action_dispatch import resolve_hardcore_action_parts
|
||||
|
||||
|
||||
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 _lowercase_for_inline_join(text: str) -> str:
|
||||
text = _clean(text)
|
||||
return text[:1].lower() + text[1:] if text else text
|
||||
|
||||
|
||||
def _with_indefinite_article(text: str) -> str:
|
||||
text = _clean(text)
|
||||
if not text or text.lower().startswith(("a ", "an ")):
|
||||
return text
|
||||
article = "an" if text[:1].lower() in "aeiou" else "a"
|
||||
return f"{article} {text}"
|
||||
|
||||
|
||||
def hardcore_action_sentence(
|
||||
role_graph: str,
|
||||
hard_item: str,
|
||||
composition: str = "",
|
||||
axis_values: Any = None,
|
||||
detail_density: str = "balanced",
|
||||
action_family: Any = "",
|
||||
) -> str:
|
||||
parts = resolve_hardcore_action_parts(role_graph, hard_item, composition, axis_values, detail_density, action_family)
|
||||
role_graph = parts.role_graph
|
||||
hard_item = parts.hard_item
|
||||
detail = parts.detail
|
||||
anchor = parts.anchor
|
||||
arrangement = hardcore_pose_arrangement(anchor, role_graph, hard_item, composition, axis_values)
|
||||
anchor_phrase = _with_indefinite_article(anchor) if anchor else ""
|
||||
if arrangement and anchor_phrase and not arrangement_duplicates_role(arrangement, role_graph):
|
||||
anchor_phrase = f"{anchor_phrase} {arrangement}"
|
||||
if role_graph and anchor_phrase:
|
||||
sentence = f"In {anchor_phrase}, {_lowercase_for_inline_join(role_graph)}"
|
||||
elif role_graph:
|
||||
sentence = role_graph
|
||||
elif detail and anchor_phrase:
|
||||
sentence = f"In {anchor_phrase}, {detail}"
|
||||
detail = ""
|
||||
else:
|
||||
sentence = detail or hard_item
|
||||
detail = ""
|
||||
if detail:
|
||||
sentence = f"{sentence}; {detail}"
|
||||
return sentence
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from . import formatter_input as input_policy
|
||||
except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.py`.
|
||||
import formatter_input as input_policy
|
||||
|
||||
|
||||
def _clean(value: Any) -> str:
|
||||
return input_policy.clean_text(value)
|
||||
|
||||
|
||||
def _with_indefinite_article(text: str) -> str:
|
||||
text = _clean(text)
|
||||
if not text or text.lower().startswith(("a ", "an ")):
|
||||
return text
|
||||
article = "an" if text[:1].lower() in "aeiou" else "a"
|
||||
return f"{article} {text}"
|
||||
|
||||
|
||||
def prompt_cast_descriptors(text: str) -> str:
|
||||
return _clean(text).replace("Woman A / primary creator:", "Woman A:")
|
||||
|
||||
|
||||
def cast_entries(text: str) -> list[tuple[str, str]]:
|
||||
text = prompt_cast_descriptors(text)
|
||||
entries: list[tuple[str, str]] = []
|
||||
for part in text.split(";"):
|
||||
part = _clean(part)
|
||||
match = re.match(r"^((?:Woman|Man) [A-Z]):\s*(.+)$", part)
|
||||
if match:
|
||||
entries.append((match.group(1), _clean(match.group(2))))
|
||||
return entries
|
||||
|
||||
|
||||
def cast_labels(text: str) -> list[str]:
|
||||
return [label for label, _descriptor in cast_entries(text)]
|
||||
|
||||
|
||||
def natural_cast_descriptor_text(text: str) -> str:
|
||||
entries = cast_entries(text)
|
||||
if not entries:
|
||||
return _clean(text)
|
||||
labels = [label for label, _descriptor in entries]
|
||||
if labels == ["Woman A"] or labels == ["Man A"]:
|
||||
return f"A {entries[0][1]}"
|
||||
if set(labels) == {"Woman A", "Man A"} and len(labels) == 2:
|
||||
by_label = {label: descriptor for label, descriptor in entries}
|
||||
return f"A {by_label['Woman A']} alongside a {by_label['Man A']}"
|
||||
return " ".join(f"{label} is {descriptor}." for label, descriptor in entries)
|
||||
|
||||
|
||||
def label_join(labels: list[str]) -> str:
|
||||
labels = [_clean(label) for label in labels if _clean(label)]
|
||||
if not labels:
|
||||
return "the named adults"
|
||||
if set(labels) == {"Woman A", "Man A"}:
|
||||
return "the woman and man"
|
||||
if len(labels) == 1:
|
||||
if labels[0] == "Woman A":
|
||||
return "the woman"
|
||||
if labels[0] == "Man A":
|
||||
return "the man"
|
||||
return labels[0]
|
||||
if len(labels) == 2:
|
||||
return f"{labels[0]} and {labels[1]}"
|
||||
return f"{', '.join(labels[:-1])}, and {labels[-1]}"
|
||||
|
||||
|
||||
def natural_label_text(text: Any, labels: list[str], *, capitalize_sentence_starts: bool = True) -> str:
|
||||
text = _clean(text)
|
||||
if not text:
|
||||
return ""
|
||||
if set(labels) == {"Woman A", "Man A"}:
|
||||
text = re.sub(r"\bWoman A\b", "the woman", text)
|
||||
text = re.sub(r"\bMan A\b", "the man", text)
|
||||
elif labels == ["Woman A"]:
|
||||
text = re.sub(r"\bWoman A\b", "the woman", text)
|
||||
elif labels == ["Man A"]:
|
||||
text = re.sub(r"\bMan A\b", "the man", text)
|
||||
if capitalize_sentence_starts:
|
||||
text = re.sub(
|
||||
r"(^|[.!?]\s+)(the woman|the man)\b",
|
||||
lambda match: match.group(1) + match.group(2).capitalize(),
|
||||
text,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def lowercase_for_inline_join(text: str) -> str:
|
||||
return re.sub(
|
||||
r"^(The woman|The man|The viewer|The named adults)\b",
|
||||
lambda match: match.group(1).lower(),
|
||||
_clean(text),
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def cast_prose(
|
||||
text: str,
|
||||
central_label: str = "Woman A",
|
||||
omit_labels: list[str] | set[str] | tuple[str, ...] = (),
|
||||
) -> tuple[str, list[str]]:
|
||||
raw_entries = cast_entries(text)
|
||||
omitted = set(omit_labels or [])
|
||||
entries = [(label, descriptor) for label, descriptor in raw_entries if label not in omitted]
|
||||
if raw_entries and not entries:
|
||||
return "", []
|
||||
if not entries:
|
||||
return (f"{central_label} is {_clean(text)}" if _clean(text) else "", [])
|
||||
labels = [label for label, _descriptor in entries]
|
||||
if labels == ["Woman A"]:
|
||||
return _with_indefinite_article(entries[0][1]), labels
|
||||
if labels == ["Man A"]:
|
||||
return _with_indefinite_article(entries[0][1]), labels
|
||||
if set(labels) == {"Woman A", "Man A"} and len(labels) == 2:
|
||||
by_label = {label: descriptor for label, descriptor in entries}
|
||||
return f"{_with_indefinite_article(by_label['Woman A'])} alongside {_with_indefinite_article(by_label['Man A'])}", labels
|
||||
sentences = []
|
||||
for label, descriptor in entries:
|
||||
sentences.append(f"{label} is {descriptor}.")
|
||||
if central_label in labels:
|
||||
sentences.append(f"{central_label} is the central subject.")
|
||||
return " ".join(sentences), labels
|
||||
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from .krea_cast import natural_label_text
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
from krea_cast import natural_label_text
|
||||
|
||||
|
||||
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 clothing_access_phrase(action_text: Any) -> str:
|
||||
text = _clean(action_text).lower()
|
||||
if any(term in text for term in ("cumshot", "ejaculat", "semen", "cum on", "cum across", "post-orgasm", "aftermath")):
|
||||
return "leaving the body exposed for visible semen and aftermath"
|
||||
if any(term in text for term in ("boobjob", "titjob", "breast sex", "handjob", "hand job", "footjob", "testicle", "balls", "penis licking", "non-penetrative")):
|
||||
return "leaving the contact point unobstructed"
|
||||
if any(term in text for term in ("oral", "blowjob", "fellatio", "mouth", "tongue")):
|
||||
return "leaving the oral contact unobstructed"
|
||||
if any(term in text for term in ("penetrat", "thrust", "penis entering", "vaginal", "anal")):
|
||||
return "leaving the penetration point unobstructed"
|
||||
return "leaving skin and body contact readable"
|
||||
|
||||
|
||||
def natural_clothing_state(text: Any, action_text: Any = "") -> str:
|
||||
text = _clean(text)
|
||||
if not text:
|
||||
return ""
|
||||
text = re.sub(r"^Clothing state:\s*", "", text, flags=re.IGNORECASE)
|
||||
if re.search(r";\s*(?=(?:Woman|Man) [A-Z]\b)", text):
|
||||
parts = [
|
||||
natural_clothing_state(part, action_text).rstrip(".")
|
||||
for part in re.split(r";\s*(?=(?:Woman|Man) [A-Z]\b)", text)
|
||||
if _clean(part)
|
||||
]
|
||||
return ". ".join(part for part in parts if part)
|
||||
body_exposure = re.match(r"^Body exposure:\s*(.*?)\.?$", text, flags=re.IGNORECASE)
|
||||
if body_exposure:
|
||||
return _clean(body_exposure.group(1)).rstrip(".")
|
||||
if re.search(r"\bfully nude\b|\bbody is fully exposed\b|\bno clothing covering\b", text, flags=re.IGNORECASE):
|
||||
owner = "the woman"
|
||||
owner_match = re.match(r"^\s*((?:Woman|Man) [A-Z])\b", text)
|
||||
if owner_match:
|
||||
owner = natural_label_text(owner_match.group(1), ["Woman A", "Man A"]) or owner
|
||||
return f"{owner.capitalize()}'s body is fully exposed, bare skin unobstructed"
|
||||
match = re.match(
|
||||
r"^(.*?)\b(?:softcore|teaser) outfit is (.*?)(?: for the (?:hardcore|sex) scene)?;\s*(?:softcore visual reference|teaser outfit detail):\s*(.*?)\.?$",
|
||||
text,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if match:
|
||||
owner = natural_label_text(match.group(1).strip(" 's"), ["Woman A", "Man A"]).strip() or "the woman"
|
||||
state = _clean(match.group(2)).lower()
|
||||
outfit = _clean(match.group(3)).rstrip(".")
|
||||
if "fully nude" in state or "fully exposed" in state or "no clothing covering" in state:
|
||||
return f"{owner.capitalize()}'s body is fully exposed, bare skin unobstructed"
|
||||
if "nude-adjacent" in state:
|
||||
return f"{owner.capitalize()}'s body is partly exposed"
|
||||
if "partially removed" in state or "pushed aside" in state:
|
||||
return f"{owner.capitalize()}'s {outfit} is pushed aside or partly removed where needed, {clothing_access_phrase(action_text)}"
|
||||
if "keeps" in state:
|
||||
return f"{owner.capitalize()} keeps the {outfit} on while {clothing_access_phrase(action_text)}"
|
||||
text = re.sub(r";\s*(?:softcore visual reference|teaser outfit detail):\s*", ". Visual clothing state: ", text, flags=re.IGNORECASE)
|
||||
text = text.replace("softcore outfit", "outfit")
|
||||
text = text.replace("teaser outfit", "outfit")
|
||||
text = text.replace("hardcore scene", "sex scene")
|
||||
return text
|
||||
@@ -0,0 +1,241 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable
|
||||
|
||||
try:
|
||||
from . import krea2_pose_variant_catalog
|
||||
except ImportError: # Allows local smoke tests with top-level imports.
|
||||
import krea2_pose_variant_catalog
|
||||
|
||||
|
||||
MOUTH_EXPRESSION_TERMS = ("mouth", "oral", "tongue", "lips", "gagging", "saliva", "drool")
|
||||
TOP_VIEW_ORAL_VARIANT = "pov_blowjob_top_down_vertical_shaft"
|
||||
ORAL_CONTACT_VARIANTS = frozenset(
|
||||
(
|
||||
TOP_VIEW_ORAL_VARIANT,
|
||||
"pov_blowjob_side_profile_oral",
|
||||
"pov_blowjob_laying_frontal_oral",
|
||||
"pov_blowjob_sitting_upright_oral",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KreaConfiguredCastRequest:
|
||||
row: dict[str, Any]
|
||||
detail_level: str
|
||||
style_mode: str
|
||||
primary: str
|
||||
item: str
|
||||
scene: str
|
||||
expression: str
|
||||
composition: str
|
||||
source_composition: str
|
||||
camera: str
|
||||
camera_scene: str
|
||||
style: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KreaConfiguredCastPrompt:
|
||||
prompt: str
|
||||
method: str = "metadata(configured_cast)"
|
||||
|
||||
def as_tuple(self) -> tuple[str, str]:
|
||||
return self.prompt, self.method
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KreaConfiguredCastDependencies:
|
||||
clean: Callable[[Any], str]
|
||||
sanitize_hardcore_environment_anchors: Callable[[Any], str]
|
||||
sanitize_hardcore_axis_values: Callable[[Any], Any]
|
||||
sanitize_scene_text_for_cast: Callable[[Any, list[str]], str]
|
||||
normalize_hardcore_detail_density: Callable[[Any], str]
|
||||
row_action_family: Callable[[Any], str]
|
||||
hardcore_action_sentence: Callable[[str, str, str, Any, str, str], str]
|
||||
pov_action_phrase: Callable[[str, list[str], str, str, str, Any, str], str]
|
||||
pov_labels_from_value: Callable[[Any], list[str]]
|
||||
merge_labels: Callable[..., list[str]]
|
||||
cast_prose_omit: Callable[[str, list[str]], tuple[str, list[str]]]
|
||||
filter_pov_labeled_clauses: Callable[[Any, list[str]], str]
|
||||
natural_label_text: Callable[[Any, list[str]], str]
|
||||
pov_composition_text: Callable[[Any, list[str]], str]
|
||||
pov_camera_phrase: Callable[[list[str]], str]
|
||||
expression_phrase: Callable[[Any], str]
|
||||
composition_phrase: Callable[..., str]
|
||||
paragraph: Callable[[list[str]], str]
|
||||
|
||||
|
||||
def _coworking_action_anchor(action_family: str, scene_text: str, action: str) -> str:
|
||||
action_lower = action.lower()
|
||||
if "office chair seat and chair arms" in action_lower:
|
||||
return ""
|
||||
scene_lower = scene_text.lower()
|
||||
if not any(term in scene_lower for term in ("coworking", "office", "desk", "laptop", "glass partition")):
|
||||
return ""
|
||||
if action_family == "climax" and "post-ejaculation open-thigh display" in action_lower:
|
||||
return (
|
||||
"office chair seat and chair arms frame the lower foreground around her open thighs, "
|
||||
"with desk edges, laptop tables, glass partitions, plants, and tall-window depth beside and behind her body"
|
||||
)
|
||||
if "broad v-frame" in action_lower and "open-thigh frame" in action_lower:
|
||||
return (
|
||||
"office chair seat and chair arms frame the lower foreground around her hips and raised knees, "
|
||||
"with desk edges, laptop tables, glass partitions, plants, and tall-window depth beside and behind her body"
|
||||
)
|
||||
if action_family != "manual":
|
||||
return ""
|
||||
return (
|
||||
"office chair seat and chair arms frame the lower foreground around her hips, "
|
||||
"with desk edges, laptop tables, glass partitions, plants, and tall-window depth beside and behind her body"
|
||||
)
|
||||
|
||||
|
||||
def _list_values(value: Any) -> list[str]:
|
||||
if isinstance(value, list):
|
||||
return [str(item) for item in value if str(item).strip()]
|
||||
if isinstance(value, str) and value.strip():
|
||||
return [part.strip() for part in value.split(",") if part.strip()]
|
||||
return []
|
||||
|
||||
|
||||
def _krea2_variant_keys(row: dict[str, Any]) -> list[str]:
|
||||
config = row.get("hardcore_position_config") if isinstance(row.get("hardcore_position_config"), dict) else {}
|
||||
axis_values = row.get("item_axis_values") if isinstance(row.get("item_axis_values"), dict) else {}
|
||||
return list(dict.fromkeys([*_list_values(config.get("krea2_variant_keys")), *_list_values(axis_values.get("krea2_variant_keys"))]))
|
||||
|
||||
|
||||
def _has_krea2_variant(row: dict[str, Any], key: str) -> bool:
|
||||
return key in _krea2_variant_keys(row)
|
||||
|
||||
|
||||
def _has_krea2_oral_contact_variant(row: dict[str, Any]) -> bool:
|
||||
return any(key in ORAL_CONTACT_VARIANTS for key in _krea2_variant_keys(row))
|
||||
|
||||
|
||||
def _has_krea2_atlas_variant(row: dict[str, Any]) -> bool:
|
||||
return any(krea2_pose_variant_catalog.get_variant(key) for key in _krea2_variant_keys(row))
|
||||
|
||||
|
||||
def _has_krea2_top_down_variant(row: dict[str, Any]) -> bool:
|
||||
for key in _krea2_variant_keys(row):
|
||||
variant = krea2_pose_variant_catalog.get_variant(key)
|
||||
geometry = " ".join(
|
||||
[str(variant.get("canonical_geometry") or ""), *[str(cue) for cue in variant.get("prompt_cues") or []]]
|
||||
).lower()
|
||||
if any(term in geometry for term in ("top-down", "top view", "top-view", "nadir", "overhead")):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _filter_expression_for_krea2_variant(row: dict[str, Any], expression: Any) -> Any:
|
||||
if not _has_krea2_oral_contact_variant(row):
|
||||
return expression
|
||||
clauses = [clause.strip() for clause in str(expression or "").split(";") if clause.strip()]
|
||||
if not clauses:
|
||||
return expression
|
||||
kept = [
|
||||
clause
|
||||
for clause in clauses
|
||||
if not any(term in clause.lower() for term in MOUTH_EXPRESSION_TERMS)
|
||||
]
|
||||
return "; ".join(kept)
|
||||
|
||||
|
||||
def _filter_camera_scene_for_krea2_variant(row: dict[str, Any], camera_scene: Any) -> str:
|
||||
text = str(camera_scene or "")
|
||||
if _has_krea2_atlas_variant(row):
|
||||
return ""
|
||||
if (_has_krea2_oral_contact_variant(row) or _has_krea2_top_down_variant(row)) and "eye-level" in text.lower():
|
||||
return ""
|
||||
return text
|
||||
|
||||
|
||||
def format_configured_cast_result(
|
||||
request: KreaConfiguredCastRequest,
|
||||
deps: KreaConfiguredCastDependencies,
|
||||
) -> KreaConfiguredCastPrompt:
|
||||
row = request.row
|
||||
subject = deps.clean(row.get("subject_phrase") or request.primary or "adult sexual scene")
|
||||
cast = deps.clean(row.get("cast_summary"))
|
||||
try:
|
||||
women_count = int(row.get("women_count") or 0)
|
||||
men_count = int(row.get("men_count") or 0)
|
||||
except (TypeError, ValueError):
|
||||
women_count = men_count = 0
|
||||
cast_descriptor_text = deps.clean(row.get("cast_descriptor_text"))
|
||||
pov_labels = deps.pov_labels_from_value(row.get("pov_character_labels"))
|
||||
camera = request.camera
|
||||
if pov_labels:
|
||||
camera = ""
|
||||
cast_prose, cast_labels = deps.cast_prose_omit(cast_descriptor_text, pov_labels)
|
||||
if not cast_labels and women_count == 1 and men_count == 1:
|
||||
cast_labels = ["Woman A", "Man A"]
|
||||
cast_labels = deps.merge_labels(cast_labels, pov_labels)
|
||||
has_atlas_variant = _has_krea2_atlas_variant(row)
|
||||
expression = "" if has_atlas_variant else _filter_expression_for_krea2_variant(row, request.expression)
|
||||
expression = deps.filter_pov_labeled_clauses(expression, pov_labels)
|
||||
expression = deps.natural_label_text(expression, cast_labels)
|
||||
composition = deps.sanitize_hardcore_environment_anchors(request.composition)
|
||||
source_composition = deps.sanitize_hardcore_environment_anchors(request.source_composition)
|
||||
role_graph = deps.sanitize_scene_text_for_cast(
|
||||
deps.sanitize_hardcore_environment_anchors(row.get("source_role_graph") or row.get("role_graph")),
|
||||
cast_labels,
|
||||
)
|
||||
item = deps.sanitize_scene_text_for_cast(
|
||||
deps.sanitize_hardcore_environment_anchors(request.item),
|
||||
cast_labels,
|
||||
)
|
||||
role_graph = deps.natural_label_text(role_graph, cast_labels)
|
||||
item = deps.natural_label_text(item, cast_labels)
|
||||
axis_values = deps.sanitize_hardcore_axis_values(row.get("item_axis_values"))
|
||||
detail_density = deps.normalize_hardcore_detail_density(row.get("hardcore_detail_density"))
|
||||
action_family = deps.row_action_family(row)
|
||||
action = deps.hardcore_action_sentence(
|
||||
role_graph,
|
||||
item,
|
||||
source_composition,
|
||||
axis_values,
|
||||
detail_density,
|
||||
action_family,
|
||||
)
|
||||
action = deps.pov_action_phrase(
|
||||
action,
|
||||
pov_labels,
|
||||
role_graph,
|
||||
item,
|
||||
source_composition,
|
||||
axis_values,
|
||||
detail_density,
|
||||
)
|
||||
scene_anchor = _coworking_action_anchor(
|
||||
action_family,
|
||||
" ".join(part for part in (request.scene, request.camera_scene, composition, source_composition) if part),
|
||||
action,
|
||||
)
|
||||
camera_scene = _filter_camera_scene_for_krea2_variant(row, request.camera_scene)
|
||||
output_composition = "" if has_atlas_variant else deps.pov_composition_text(composition, pov_labels)
|
||||
parts = [
|
||||
action,
|
||||
scene_anchor,
|
||||
deps.pov_camera_phrase(pov_labels),
|
||||
cast_prose,
|
||||
f"A consensual explicit adult scene with {subject}" if not action else "",
|
||||
f"The cast includes {cast}" if cast and not cast_prose and not (women_count == 1 and men_count == 1) else "",
|
||||
f"The setting is {request.scene}" if request.scene else "",
|
||||
camera_scene,
|
||||
deps.expression_phrase(expression),
|
||||
deps.composition_phrase(output_composition, action, "The image is framed as", detail_density),
|
||||
camera,
|
||||
"" if has_atlas_variant else request.style if request.detail_level != "concise" else "",
|
||||
]
|
||||
return KreaConfiguredCastPrompt(deps.paragraph(parts))
|
||||
|
||||
|
||||
def format_configured_cast(
|
||||
request: KreaConfiguredCastRequest,
|
||||
deps: KreaConfiguredCastDependencies,
|
||||
) -> tuple[str, str]:
|
||||
return format_configured_cast_result(request, deps).as_tuple()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user