diff --git a/README.md b/README.md index d3accd4..de5487e 100644 --- a/README.md +++ b/README.md @@ -419,9 +419,12 @@ generic Qwen camera views do not add `phone hidden` or other phone wording. 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 and classical library/book-stack -spaces: front/side/back views, zoom, and elevation change which desks, windows, -partitions, bookshelves, reading tables, lamps, or aisles are kept visible. In +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`, diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 3fd205c..0294f5e 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -276,9 +276,10 @@ Already isolated: 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 lives in `scene_camera_adapters.py`; row-level camera - insertion, contextual coworking composition mutation, subject-kind detection, - and POV suppression live in `row_camera.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 diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index bdf9769..f9e1ddb 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -126,8 +126,8 @@ Core helper ownership: | `hardcore_action_metadata.py` | Source action-family and position-family metadata used by Krea2, SDXL, and caption routes. | | `route_metadata.py` | Shared row-level route metadata readers for normalized action family, position family/keys, and formatter hints used by Krea2, SDXL, and caption routes. | | `pov_policy.py` | Shared POV slot detection, POV label merging/filtering, builder POV directives, source role-graph viewer replacement, and shared POV composition cleanup used by builder and Krea2 routes. | -| `scene_camera_adapters.py` | Location-aware camera/scene prose such as coworking lounge camera layout. | -| `row_camera.py` | Row-level camera insertion, contextual coworking composition mutation, subject-kind detection, POV label fallback, and POV suppression of normal camera directives. | +| `scene_camera_adapters.py` | Location-aware camera/scene prose for coworking, library, semi-public corridor/garage/archive/etc. profiles, metadata-first profile resolution, and camera-aware composition cleanup. | +| `row_camera.py` | Row-level camera insertion, contextual scene composition mutation, subject-kind detection, POV label fallback, and POV suppression of normal camera directives. | | `krea_row_fields.py` | Shared Krea normal-row field extraction for item, scene, pose, expression, composition/source-composition, camera, and style used by normal and configured-cast routes. | | `krea_cast.py` | Shared formatter cast descriptor parsing, cast labels, cast prose, natural cast descriptor text, and label replacement used by Krea2 and caption routes. | | `prompt_hygiene.py` | Generic prompt, caption, and negative-prompt cleanup, including route-agnostic negative-prompt merge/dedupe. | @@ -695,8 +695,11 @@ Current camera-aware scene adapter: `scene_camera_profile` objects with `key`, `family`, `layout_label`, `place`, `foreground`, `midground`, `background`, `detail_label`, and optional per-subject `composition` text. -- Coworking/business-cafe/office scenes and classical library/book-stack scenes - are detected by `scene_camera_profile`. +- Coworking/business-cafe/office scenes, classical library/book-stack scenes, + and semi-public repeating-structure scenes such as hotel corridors, parking + garages, archives, laundromats, station lockers, backstage halls, wine + cellars, nightclub back halls, and restaurant booths are detected by + `scene_camera_profile`. - Location themes preserve `theme` on configs and selected scene entries, and rows expose `location_theme`, `scene_theme`, `composition_theme`, and `scene_camera_profile_key` for debugging and future route rules. diff --git a/scene_camera_adapters.py b/scene_camera_adapters.py index a5a0daa..8e20bf1 100644 --- a/scene_camera_adapters.py +++ b/scene_camera_adapters.py @@ -110,12 +110,260 @@ SCENE_CAMERA_PROFILES: tuple[dict[str, Any], ...] = ( "default": "classical library frame with the subjects near a bookshelf edge and long shelf depth behind them", }, }, + { + "key": "hotel_corridor", + "family": "semi_public", + "terms": ( + "hotel corridor", + "hotel service corridor", + "hotel service alcove", + "service alcove", + "service hallway", + "service hall", + "repeating numbered doors", + "numbered doors", + "luggage carts", + "stair landing", + "hotel stair landing", + ), + "layout_label": "Hotel corridor camera layout", + "place": "hotel corridor", + "foreground": "nearest doorframe edge, patterned carpet line, and wall sconce", + "midground": "repeating numbered doors, brass wall lamps, service-alcove turns, and luggage carts", + "background": "long corridor perspective, closed doors, warm late-night depth, and quiet hotel sightlines", + "detail_label": "hotel corridor details", + "composition": { + "woman": "hotel corridor frame with the woman near a doorframe edge and repeated doors behind her", + "man": "hotel corridor frame with the man near a doorframe edge and repeated doors behind him", + "default": "hotel corridor frame with the subjects near a doorframe edge and repeated doors behind them", + }, + }, + { + "key": "parking_garage", + "family": "semi_public", + "terms": ( + "parking garage", + "parking deck", + "underground garage", + "multi-level parking", + "concrete pillars", + "numbered pillars", + "painted floor lines", + "painted bay lines", + "parked cars", + ), + "layout_label": "Parking garage camera layout", + "place": "parking garage", + "foreground": "nearest concrete pillar, painted floor line, and car bumper edge", + "midground": "repeating concrete pillars, parked cars, painted bay lines, and glossy concrete lanes", + "background": "shadowed corners, fluorescent depth, numbered pillars, and long garage perspective", + "detail_label": "parking garage details", + "composition": { + "woman": "parking garage frame with the woman beside a concrete pillar and repeated bay lines behind her", + "man": "parking garage frame with the man beside a concrete pillar and repeated bay lines behind him", + "default": "parking garage frame with the subjects beside a concrete pillar and repeated bay lines behind them", + }, + }, + { + "key": "theater_backstage", + "family": "semi_public", + "terms": ( + "theater backstage", + "backstage wings", + "cabaret backstage", + "prop storage", + "prop racks", + "costume racks", + "costume rails", + "velvet curtains", + "stage ropes", + "scenery flats", + ), + "layout_label": "Backstage camera layout", + "place": "theater backstage", + "foreground": "curtain edge, prop trunk corner, and costume-rack line", + "midground": "layered velvet curtains, costume racks, prop shelves, and vanity bulb mirrors", + "background": "dark stage wings, repeated scenery flats, narrow backstage passages, and warm light spill", + "detail_label": "backstage details", + "composition": { + "woman": "backstage frame with the woman partly framed by curtains and costume racks behind her", + "man": "backstage frame with the man partly framed by curtains and costume racks behind him", + "default": "backstage frame with the subjects partly framed by curtains and costume racks behind them", + }, + }, + { + "key": "wine_cellar", + "family": "semi_public", + "terms": ( + "wine cellar", + "wine storage", + "bottle racks", + "bottle shelves", + "arched cellar", + "brick niches", + "cellar corridor", + "stacked bottle", + ), + "layout_label": "Wine cellar camera layout", + "place": "wine cellar", + "foreground": "near bottle-rack edge, crate corner, and stone floor line", + "midground": "repeating bottle racks, arched brick niches, narrow aisles, and low amber lamps", + "background": "cool shadowed depth, stacked shelves, cellar arches, and secluded rack rows", + "detail_label": "wine cellar details", + "composition": { + "woman": "wine cellar frame with the woman between bottle racks and arched cellar depth behind her", + "man": "wine cellar frame with the man between bottle racks and arched cellar depth behind him", + "default": "wine cellar frame with the subjects between bottle racks and arched cellar depth behind them", + }, + }, + { + "key": "museum_archive", + "family": "semi_public", + "terms": ( + "museum archive", + "gallery storage", + "rare-books archive", + "archive room", + "storage shelves", + "labeled boxes", + "rolling shelves", + "catalog drawers", + "compact shelving", + ), + "layout_label": "Archive camera layout", + "place": "museum archive", + "foreground": "storage-shelf edge, archive box corner, and work-table line", + "midground": "labeled boxes, rolling shelves, frame racks, catalog drawers, and long work tables", + "background": "compact shelving rows, soft overhead lights, archival aisles, and hidden storage depth", + "detail_label": "archive details", + "composition": { + "woman": "archive frame with the woman beside labeled storage shelves and compact rows behind her", + "man": "archive frame with the man beside labeled storage shelves and compact rows behind him", + "default": "archive frame with the subjects beside labeled storage shelves and compact rows behind them", + }, + }, + { + "key": "laundromat_late_night", + "family": "semi_public", + "terms": ( + "laundromat", + "coin laundry", + "washing machines", + "stacked dryers", + "washer-door", + "washer door", + "folding tables", + "detergent shelves", + "machine row", + ), + "layout_label": "Laundromat camera layout", + "place": "late-night laundromat", + "foreground": "folding-table edge, chrome washer door, and tiled floor line", + "midground": "repeating washing machines, stacked dryers, detergent shelves, and empty machine rows", + "background": "cool fluorescent depth, mirrored machine doors, front glass, and quiet back-corner sightlines", + "detail_label": "laundromat details", + "composition": { + "woman": "laundromat frame with the woman near a folding table and repeated washer doors behind her", + "man": "laundromat frame with the man near a folding table and repeated washer doors behind him", + "default": "laundromat frame with the subjects near a folding table and repeated washer doors behind them", + }, + }, + { + "key": "train_station_lockers", + "family": "semi_public", + "terms": ( + "train-station locker", + "train station locker", + "locker corridor", + "station underpass", + "station service passage", + "metal lockers", + "vending machines", + "tiled walls", + "utility doors", + "warning stripes", + ), + "layout_label": "Station locker camera layout", + "place": "train-station locker corridor", + "foreground": "locker edge, vending-machine corner, and tiled floor line", + "midground": "repeating metal lockers, tiled wall seams, poster frames, and utility doors", + "background": "fluorescent underpass depth, stair railings, warning stripes, and hidden side alcoves", + "detail_label": "station locker details", + "composition": { + "woman": "station locker frame with the woman beside metal lockers and tiled depth behind her", + "man": "station locker frame with the man beside metal lockers and tiled depth behind him", + "default": "station locker frame with the subjects beside metal lockers and tiled depth behind them", + }, + }, + { + "key": "nightclub_back_hall", + "family": "semi_public", + "terms": ( + "nightclub back hallway", + "club vip corridor", + "vip club corridor", + "music venue greenroom", + "greenroom corridor", + "coat-check racks", + "neon strips", + "velvet ropes", + "mirrored wall panels", + "stickered doors", + ), + "layout_label": "Nightclub back-hall camera layout", + "place": "nightclub back hallway", + "foreground": "black door edge, velvet-rope post, and mirrored wall strip", + "midground": "repeated dark doors, neon strips, coat-check racks, mirrored panels, and booth edges", + "background": "distant colored dance-floor light, dim practical lamps, cable cases, and narrow hallway depth", + "detail_label": "nightclub back-hall details", + "composition": { + "woman": "nightclub back-hall frame with the woman near a dark door edge and neon hallway depth behind her", + "man": "nightclub back-hall frame with the man near a dark door edge and neon hallway depth behind him", + "default": "nightclub back-hall frame with the subjects near a dark door edge and neon hallway depth behind them", + }, + }, + { + "key": "restaurant_private_booth", + "family": "semi_public", + "terms": ( + "restaurant private booth", + "private booth", + "bistro back corner", + "after-hours dining", + "afterhours dining", + "high banquettes", + "dark wood partitions", + "folded napkins", + "stacked chairs", + "small round tables", + ), + "layout_label": "Restaurant booth camera layout", + "place": "restaurant private booth", + "foreground": "table edge, high banquette back, and dark wood partition", + "midground": "repeating table lamps, folded napkins, mirrored wall panels, and empty tables", + "background": "after-hours dining-room depth, stacked chairs, service doorway, and secluded sightlines", + "detail_label": "restaurant booth details", + "composition": { + "woman": "restaurant booth frame with the woman beside a high banquette and table lamps behind her", + "man": "restaurant booth frame with the man beside a high banquette and table lamps behind him", + "default": "restaurant booth frame with the subjects beside a high banquette and table lamps behind them", + }, + }, ) SCENE_CAMERA_PROFILE_KEYS = {str(profile["key"]): dict(profile) for profile in SCENE_CAMERA_PROFILES} THEME_PROFILE_KEYS = { "classical_library": "classical_library", + "hotel_corridor": "hotel_corridor", + "parking_garage": "parking_garage", + "theater_backstage": "theater_backstage", + "wine_cellar": "wine_cellar", + "museum_archive": "museum_archive", + "laundromat_late_night": "laundromat_late_night", + "train_station_lockers": "train_station_lockers", + "nightclub_back_hall": "nightclub_back_hall", + "restaurant_private_booth": "restaurant_private_booth", } PROFILE_TEXT_FIELDS = ( @@ -348,7 +596,7 @@ def coworking_location_profile(scene_text: Any) -> dict[str, str]: return scene_camera_profile("coworking lounge") -def coworking_subject_terms(subject_kind: str, pov_labels: list[str] | None = None) -> tuple[str, str]: +def scene_subject_terms(subject_kind: str, pov_labels: list[str] | None = None) -> tuple[str, str]: if pov_labels: return "the visible partner", "them" if subject_kind == "woman": @@ -360,6 +608,10 @@ def coworking_subject_terms(subject_kind: str, pov_labels: list[str] | None = No return "the subjects", "them" +def coworking_subject_terms(subject_kind: str, pov_labels: list[str] | None = None) -> tuple[str, str]: + return scene_subject_terms(subject_kind, pov_labels) + + def scene_direction_detail( direction: str, profile: dict[str, str], @@ -371,7 +623,7 @@ def scene_direction_detail( midground = profile["midground"] background = profile["background"] detail_label = profile.get("detail_label") or "location details" - subject, pronoun = coworking_subject_terms(subject_kind, pov_labels) + subject, pronoun = scene_subject_terms(subject_kind, pov_labels) if pov_labels: if "right side" in direction: return f"{subject} is in right-side profile; {midground} run behind {pronoun} toward {background}, with {detail_label} kept at the frame edges" @@ -411,7 +663,7 @@ def scene_distance_detail( pov_labels: list[str] | None = None, ) -> str: distance = str(distance or "").strip().lower() - subject, _pronoun = coworking_subject_terms(subject_kind, pov_labels) + subject, _pronoun = scene_subject_terms(subject_kind, pov_labels) if pov_labels: if "wide" in distance or "full-body" in distance or "full body" in distance: return f"wide POV keeps {subject} readable with {profile['place']} context behind them" @@ -441,7 +693,7 @@ def scene_elevation_detail( pov_labels: list[str] | None = None, ) -> str: elevation = str(elevation or "").strip().lower() - subject, pronoun = coworking_subject_terms(subject_kind, pov_labels) + subject, pronoun = scene_subject_terms(subject_kind, pov_labels) if pov_labels: if "low-angle" in elevation: return f"low angle keeps POV body cues low while the {profile['background']} rises behind {pronoun}" @@ -494,13 +746,13 @@ def scene_camera_directive( direction_detail = scene_direction_detail(direction, profile, pov_labels, subject_kind) distance_detail = scene_distance_detail(distance, profile, subject_kind, pov_labels) elevation_detail = scene_elevation_detail(elevation, profile, subject_kind, pov_labels) - if pov_labels: - return ( - f"{profile['layout_label']} from POV: {direction_detail}. " - f"{distance_detail}; {elevation_detail}; use the multiangle camera only as first-person spatial geometry." - ) geometry = camera_geometry_phrase(parsed, compact_labels) geometry_clause = f" ({geometry})" if geometry else "" + if pov_labels: + return ( + f"{profile['layout_label']} from POV{geometry_clause}: {direction_detail}. " + f"{distance_detail}; {elevation_detail}; use the multiangle camera only as first-person spatial geometry." + ) return ( f"{profile['layout_label']}{geometry_clause}: {direction_detail}; " f"{distance_detail}; {elevation_detail}." diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 78ab414..61003f3 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -406,10 +406,14 @@ def _coworking_location_config() -> str: def _classical_library_theme_configs() -> tuple[str, str]: + return _thematic_location_configs("classical_library") + + +def _thematic_location_configs(theme: str) -> tuple[str, str]: location_config, composition_config, _summary = pb.build_thematic_location_json( enabled=True, combine_mode="replace", - theme="classical_library", + theme=theme, ) return location_config, composition_config @@ -694,6 +698,39 @@ def smoke_row_camera_policy() -> None: _expect("bag" not in library_composition.lower(), "row camera library composition leaked bag wording") _expect("shoes" not in library_composition.lower(), "row camera library composition leaked shoes wording") _expect("library" in library_composition.lower(), "row camera library composition did not become location-aware") + semi_public_row = { + "prompt": "A generated adult prompt. Composition: vertical polished mirror view with bag and shoes visible. Avoid: low quality.", + "caption": "sxcppnl7, generated adult prompt, polished mirror view with bag and shoes visible, illustration", + "scene_text": "upscale hotel corridor with repeating numbered doors, patterned carpet, brass wall lamps, luggage carts, and a secluded corner near a service alcove", + "scene_entry": { + "slug": "hotel_corridor_affair", + "prompt": "upscale hotel corridor with repeating numbered doors, patterned carpet, brass wall lamps, luggage carts, and a secluded corner near a service alcove", + "theme": "semi_public_affair", + }, + "scene_theme": "semi_public_affair", + "composition": "polished mirror view with bag and shoes visible", + "subject_type": "configured_cast", + "women_count": 1, + "men_count": 1, + "pov_character_labels": ["Man A"], + } + updated_semi_public = row_camera.apply_camera_config( + semi_public_row, + _orbit_camera(horizontal_angle=180, vertical_angle=30, zoom=7.5), + compact_labels=pb.CAMERA_COMPACT_LABELS, + ) + semi_public_scene = _expect_text("row_camera_policy.semi_public_scene", updated_semi_public.get("camera_scene_directive"), 40) + semi_public_composition = _expect_text( + "row_camera_policy.semi_public_composition", + updated_semi_public.get("composition"), + 20, + ) + _expect("Hotel corridor camera layout from POV" in semi_public_scene, "row camera semi-public scene did not use hotel corridor profile") + _expect("back view" in semi_public_scene, "row camera semi-public scene missed orbit direction") + _expect("first-person spatial geometry" in semi_public_scene, "row camera semi-public POV scene lost first-person geometry") + _expect(updated_semi_public.get("scene_camera_profile_key") == "hotel_corridor", "row camera semi-public scene did not expose text-matched profile key") + _expect("hotel corridor" in semi_public_composition.lower(), "row camera semi-public composition did not become location-aware") + _expect("bag" not in semi_public_composition.lower() and "shoes" not in semi_public_composition.lower(), "row camera semi-public composition leaked outfit-check props") metadata_profile_row = { "prompt": "A generated adult prompt. Composition: vertical polished mirror view with bag and shoes visible. Avoid: low quality.", "caption": "sxcppnl7, generated adult prompt, polished mirror view with bag and shoes visible, illustration", @@ -870,6 +907,42 @@ def smoke_config_route_location_theme() -> None: _expect("315-degree front-left quarter view" in prompt, "Krea config route lost camera directive") _expect_formatter_outputs(row, "config_route_location_theme", target="single") + parking_location_config, parking_composition_config = _thematic_location_configs("parking_garage") + parking_row = pb.build_prompt_from_configs( + row_number=1, + start_index=1, + seed=3311, + category_config=pb.build_category_config_json("woman", "random"), + cast_config=pb.build_cast_config_json("solo_woman", 1, 0), + generation_profile=pb.build_generation_profile_json(profile="balanced"), + camera_config=_orbit_camera( + horizontal_angle=135, + vertical_angle=-30, + zoom=4.0, + subject_focus="environment", + ), + location_config=parking_location_config, + composition_config=parking_composition_config, + ) + _expect_row_base(parking_row, "config_route_location_theme.parking") + parking_scene = _expect_text("config_route_location_theme.parking_scene", parking_row.get("scene_text"), 20) + parking_composition = _expect_text("config_route_location_theme.parking_composition", parking_row.get("composition"), 10) + parking_directive = _expect_text( + "config_route_location_theme.parking_camera_scene_directive", + parking_row.get("camera_scene_directive"), + 40, + ) + parking_profile = parking_row.get("scene_camera_profile") if isinstance(parking_row.get("scene_camera_profile"), dict) else {} + _expect("parking" in parking_scene.lower() or "garage" in parking_scene.lower(), "parking theme did not drive scene") + _expect("parking" in parking_composition.lower() or "garage" in parking_composition.lower() or "pillar" in parking_composition.lower(), "parking theme did not drive composition") + _expect(parking_row.get("location_theme") == "parking_garage", "parking location theme did not survive") + _expect(parking_row.get("scene_theme") == "parking_garage", "parking scene theme did not survive") + _expect(parking_row.get("scene_camera_profile_key") == "parking_garage", "parking theme did not expose camera profile key") + _expect(parking_profile.get("family") == "semi_public", "parking camera profile family should be semi_public") + _expect("Parking garage camera layout" in parking_directive, "parking theme did not drive camera-scene adapter") + _expect("back-right quarter view" in parking_directive, "parking camera-scene adapter missed orbit direction") + _expect("low-angle shot" in parking_directive, "parking camera-scene adapter missed elevation") + def smoke_builder_prompt_route_policy() -> None: def legacy_from_request(request: builder_prompt_route.PromptBuildRequest) -> dict[str, Any]: