# 08 — Diseño de stages, vestuario y cámara

Fecha: 20-10-2026
Estado: **diseño aprobado, sin implementar**. No se ha gastado GPU para este spec.

## 1. Problema

El pack de 45 de `Makima_v2_oficina` dejó tres problemas visibles:

1. **La ropa cambia entre imágenes del mismo pack.** El planner usa
   `--variation per-image` por defecto y el outfit se sortea por job. Resultado
   medido: 10 `office_lady`, 5 `suit`, 16 `unbuttoned` y 14 sin outfit.
2. **`spicy` repite un solo outfit.** `oficina.spicy` solo tiene `unbuttoned`;
   por eso las 16 imágenes spicy comparten la misma camisa medio abierta.
3. **La pose pierde contra el tema.** `lying_side`, `missionary` y
   `against_wall` se convirtieron en de pie/sentado/cargando en `oficina`, porque
   el tema no ofrece superficie o pared compatible.

Además, el encuadre no está controlado por etapa: la lista de cámaras no incluye
`full body`, pero el modelo igual compone cuerpos completos cuando la pose o la
escena pesan más que el encuadre.

## 2. Decisiones cerradas

- **`nivel` y `stage` son ejes separados.** `nivel` sigue siendo
  `sugerente|spicy|explicito` y manda en pools, banco, seguridad y compatibilidad.
  `stage` añade cinco escalones de contenido.
- **Un outfit base por pack, elegido una vez.** Los stages solo añaden o quitan
  capas; no vuelven a sortear ropa.
- **Tags primero.** Los LoRAs de ropa quedan para después; si se usan, peso bajo
  (0.5–0.7) y auditoría de identidad.
- **Cámara por stage.** Cada stage tiene su allowlist; `from side` deja de
  forzarse y pasa a ser un recurso, no una norma.
- **Pose ↔ escena se mapea como validación.** No se inyectan objetos
  (`on_bed`); como mucho, una precisión relacional (`against_wall -> wall`) si el
  tema ya tiene pared. En v1 solo avisa; no reescribe el prompt.

## 3. Modelo de stages

```python
STAGES = ("sugerente", "spicy1", "spicy2", "explicit1", "explicit2")

STAGE_LEVEL = {
    "sugerente": "sugerente",
    "spicy1": "spicy",
    "spicy2": "spicy",
    "explicit1": "explicito",
    "explicit2": "explicito",
}

STAGE_DRESS = {
    "sugerente": "dressed",
    "spicy1": "stripping",
    "spicy2": "lingerie",
    "explicit1": "clothed",
    "explicit2": "nude",
}

DEFAULT_STAGE_QUOTAS = {
    "sugerente": 9,
    "spicy1": 9,
    "spicy2": 9,
    "explicit1": 9,
    "explicit2": 9,
}
```

| stage | nivel | dress | contenido |
|---|---|---|---|
| `sugerente` | sugerente | `dressed` | pose coqueta, ropa completa |
| `spicy1` | spicy | `stripping` | desvestimiento parcial, foco pierna/pecho/muslo |
| `spicy2` | spicy | `lingerie` | half naked, sujetador/bragas visibles |
| `explicit1` | explicito | `clothed` | sexo con ropa/lencería, sin cum ni orgasm |
| `explicit2` | explícito | `nude` | sexo completo, variedad, cum/orgasm |

Reglas:

- `explicit1` excluye del pool/estado `cum`, `cum_overflow`, `orgasm`,
  `after_sex` y equivalentes. `explicit2` los permite.
- `nivel` sigue decidiendo qué poses del banco y qué pools de exploración se
  usan; `stage` decide el vestuario, el encuadre, el acto y la cuota.
- Un stage no puede usar una pose de otro nivel. `spicy2` no toma poses de
  `explicito`.

## 4. Vestuario

### 4.1 Schema de `themes.json`

Se añade una clave opcional `outfit_story`. La clave exterior es el **outfit
base**; los tags base salen de `outfit.sugerente[base]` o, si no existe, de
`outfit.spicy[base]`.

```json
"outfit_story": {
  "office_lady": {
    "spicy1": ["unbuttoned shirt", "skirt lift"],
    "spicy2": ["black bra", "black panties", "shirt open"],
    "explicit1": ["black bra", "black panties", "clothed sex"]
  },
  "suit": {
    "spicy1": ["unbuttoned shirt", "blazer open"],
    "spicy2": ["black bra", "black panties", "pencil skirt"],
    "explicit1": ["black bra", "black panties", "clothed sex"]
  }
}
```

- `sugerente` usa el outfit base sin modificadores.
- `explicit2` no lleva outfit; va `nude`.
- `outfit_story` puede tener varios bases; el pack elige **uno solo**.
- Si existe `outfit_story`, debe cubrir `spicy1`, `spicy2` y `explicit1` para
  cada base. Si falta un stage, es un error de datos, no un fallback silencioso.
- Si no existe `outfit_story`, el pack de 5 stages no se genera por defecto para
  ese tema. Se permite el modo legacy explícito con `--allow-legacy-outfit`, que
  usa el outfit del nivel y marca `outfit_fallback=true` en el manifest.
- Los modificadores son del tema, no genéricos. Un `bra` genérico en playa
  puede pelear con el bikini.
- Los tags de `outfit_story` no pueden ser escena, luz, props ni personaje.
- El LoRA de ropa queda fuera de v1. Campo futuro: `outfit_lora` por base.
  Si se usa: `<lora:...:0.5-0.7>` y auditoría obligatoria de cara/cuerpo.

### 4.2 Resolución del outfit

```text
base = _choose_base(outfit_story, --variant outfit, --variation)
tags_base = outfit.sugerente[base] or outfit.spicy[base]
tags_stage = outfit_story[base].get(stage, [])
outfit_line = tags_base + tags_stage   # sugerente: solo base
explicit2: outfit_line = ""
```

- El outfit base se elige **una vez por pack**. `--variation` deja de afectar al
  outfit: solo afecta a `scene`, `lighting` y `props`.
- `--variant outfit=office_lady` fija el base.
- Sin `--variant`, se elige una vez con el RNG del pack.
- Todos los jobs del pack comparten `outfit_key` y `outfit_base`.
- Si el personaje está en `nude` (`explicit2`), no se escribe outfit en el
  positive; `force_nude="always"`.

### 4.3 `dress` y negative

`--dress` mantiene `dressed|stripping|nude` y añade `lingerie|clothed`. `auto`
usa `STAGE_DRESS[stage]`.

```python
FORCE_NUDE = {
    "dressed": "never",
    "stripping": "never",
    "lingerie": "never",
    "clothed": "never",
    "nude": "always",
    "auto": "auto",
}
```

El negative de vestuario deja de ser un único `nipples, topless`:

- `dressed` / `stripping`: `nipples, topless`.
- `lingerie`: `topless, bottomless` (se espera bra/bragas cubriendo).
- `clothed`: `full nudity` como primera propuesta; **a calibrar**.
- `nude`: sin bloque de vestuario.

No se usan `nude`, `naked`, `nsfw` ni `explicit` en el negative (regla del
proyecto). La calibración exacta de `lingerie`/`clothed` se hará con imágenes,
no en este spec.

### 4.4 Mangas: el outfit tiene que declararlas

Regla medida el 26-10-2026. **Un outfit que deba cubrir los brazos necesita su
tag de manga explícito.** `sweater, skirt, thighhighs` no dice "de manga larga":
en cuanto la pose sube los brazos (`arms_up`), el modelo interpreta *suéter sin
mangas* y puede incluso dejar la espalda al aire.

```text
"biblioteca": outfit.sugerente.sweater = [sweater, long sleeves, skirt, thighhighs, glasses]
```

- `long_sleeves` es tag Danbooru (1.985.620 posts). También `short_sleeves`,
  `puffy short sleeves`, `detached sleeves`.
- **`sleeveless` es lo contrario**: no lo añadas "por si acaso", ni lo quites de
  una línea de personaje sin mirar (ver `04-decisiones.md`).
- El **estado de ropa** lo dicta el outfit y el `dress`, nunca la línea de pose:
  `bare_shoulders` dentro de una pose desnuda los hombros del outfit.
- **`backless` no existe** como tag. Si hace falta repeler espalda al aire:
  `bare_back, off_shoulder, sleeveless, strapless` en el **negative**.
- `sideboob` sí es legítimo: la pose spicy lo pide y el corte lateral es
  intencionado. No se persigue.

## 5. Cámara

Nuevo archivo de datos: `Archivos/stage_cameras.json`.

```json
{
  "sugerente": ["from front", "cowboy shot", "upper body", "close-up", "portrait", "cleavage_focus"],
  "spicy1": ["from front", "from above", "thighs", "midriff", "cleavage_focus", "cowboy shot", "upper body"],
  "spicy2": ["from front", "from side", "thighs", "midriff", "cleavage_focus", "cowboy shot"],
  "explicit1": ["from front", "from side", "cowboy shot", "thighs", "midriff"],
  "explicit2": ["from front", "from side", "cowboy shot", "thighs", "midriff", "close-up"]
}
```

- El loader cae a `cameras.txt` si el archivo falta.
- `assign_cameras(job)` usa la allowlist del stage, no la lista global.
- `from side` **no se fuerza**. Solo está permitido en `spicy2`, `explicit1` y
  `explicit2`, y entra en la rotación normal. `camera_ok()` sigue evitando
  ángulos que anulan la pose (por ejemplo `back_view` sobre `lying`), pero
  `FORCE_SIDE` desaparece: no se reasigna la cámara a `from side` por sistema.
- `full body` y `wide shot` no están en la allowlist. Para `sugerente`,
  `spicy1` y `spicy2` se puede añadir `full body, wide shot` al negative como
  refuerzo de encuadre; no se hace en `explicit` sin medirlo.
- El encuadre no debe tapar la pose ni el outfit: si la pose es horizontal, la
  cámara puede ser `cowboy shot` o `thighs`, no `portrait`.

## 6. Mapa pose ↔ escena

Nuevo archivo: `Archivos/pose_scene_affinity.yaml`. Es una tabla de validación,
no un inyector de objetos.

```yaml
version: 1
poses:
  against_wall:
    requires: [wall]
    compatible: [office, bathroom, alley, bedroom]
    conflicts: [beach, poolside, forest]
  lying:
    requires_any: [bed, sofa, desk, floor, grass, sand]
    compatible: [bedroom, office, beach, forest]
    conflicts: [pool, water]
  on_side:
    requires_any: [bed, sofa, desk, floor, grass, sand]
  on_back:
    requires_any: [bed, sofa, desk, floor, grass, sand]
  missionary:
    requires_any: [bed, sofa, desk, floor, grass, sand]
  kneeling:
    requires_any: [floor, bed, grass, tatami]
  seiza:
    requires_any: [floor, bed, grass, tatami]
  cowgirl_position:
    requires_any: [bed, sofa, chair, desk]
  straddling:
    requires_any: [bed, sofa, chair, desk]
  standing_sex:
    compatible: [office, bathroom, alley, wall]
  sex_from_behind:
    compatible: [office, bathroom, alley, wall]
  full_nelson:
    compatible: [bedroom, office, outdoors]
    conflicts: [narrow, against_wall]
  suspended_congress:
    compatible: [bedroom, office, outdoors]
    conflicts: [narrow, against_wall]
```

Reglas:

- `requires` exige el tag; `requires_any` exige al menos uno; `conflicts` avisa.
- El matching normaliza `_` a espacio y minúsculas.
- Por defecto, un choque es **warning** y el job se genera.
- `--strict-scene` convierte los choques en exclusión del job.
- `against_wall` puede usar `wall` como precisión si el tema ya tiene pared
  (`scene` incluye `wall` o `office`/`bathroom`/`alley`). No se añaden camas,
  sofás ni otros objetos.
- `on_bed` no se inyecta nunca; si una pose lo necesita, el tema debe traer
  `bed` de verdad.

### 6.1 Caso conocido: `pb0035`, `pb0036`, `pb0037`

Se quedan en el pack de 45; están aprobadas como contenido y no se regeneran.

- `pb0035` `lying_side` + `oficina` + `pencil skirt` → de pie inclinada.
- `pb0036` `missionary` + `oficina` + `desk/chair` → sexo sentado/cargando.
- `pb0037` `against_wall` + `oficina` sin `wall` → sexo de pie/cargando.

El mapa las marca como **pose incompatible con el tema**, no como pose mala. La
validación debe avisar antes de renderizar para no repetir el caso.

## 7. Flujo de un pack

```text
1. cargar stages, quotas, tema, banco, pools
2. elegir base de outfit UNA vez por pack
3. por stage:
   a. nivel = STAGE_LEVEL[stage]
   b. pose = banco[nivel] + explore[nivel]
   c. dress = STAGE_DRESS[stage]
   d. outfit_line = base + mods[stage]  (explicit2 = vacío)
   e. escena/luz/props según --variation (no el outfit)
   f. cámara = allowlist[stage] + camera_ok(pose)
   g. validar pose↔escena (warning o exclusión con --strict-scene)
4. manifest: nivel + stage + outfit_base + outfit_key + outfit_fallback
5. metrics: by_level + by_stage
6. filename: <orden>_<stage>_<pose_id|new>_s<seed>.png
```

## 8. Compatibilidad y migración

- **CLI**: `--stages 9,9,9,9,9` (cinco enteros, en el orden de `STAGES`).
  Default: `9,9,9,9,9`.
- **`--levels`**: se mantiene para packs legacy. Si se pasa, define el total por
  nivel y cada nivel reparte su cuota entre sus stages; el remanente va a los
  primeros stages. `--stages` y `--levels` no se pueden usar a la vez.
- **`--variation`**: sigue existiendo para `scene`, `lighting` y `props`. El
  outfit queda siempre fijo por pack. Esto cambia el comportamiento actual de
  `--variation per-image`; es intencionado.
- **`--dress`**: `auto` recomendado. Un `--dress` explícito sobreescribe el
  stage y emite aviso; sirve para pruebas de una sola etapa, no para packs.
- **Packs viejos**: sin `stage`, se leen como antes. `stage` ausente no rompe
  `select_x_posts` ni la auditoría.
- **`select_x_posts.py`**: pasa de `SFW_LEVELS` a stages.
  `SFW_STAGES = ("sugerente", "spicy1")`;
  `EXPLICIT_STAGES = ("spicy2", "explicit1", "explicit2")`.
  Así `spicy2` no se publica como SFW. Si el usuario prefiere otra frontera, se
  cambia en un solo sitio. Para packs viejos sin `stage`, se usa el fallback
  `sugerente -> sugerente`, `spicy -> spicy1`, `explicito -> explicit2`.
- **`danbooru_harvest.validate_theme`**: sigue validando `outfit` por nivel.
  `outfit_story` se valida aparte en `pack_builder`; no se añade a `THEME_AXES`
  para no romper la regeneración de temas.
- **`pose_bank` / `pose_calibrate` / `pose_harvest`**: sin cambios; siguen
  trabajando con los 3 niveles.
- **Documentación**: `07-estado-pipeline.md` aún tiene referencias a 55 y 34;
  `AGENTS.md` está parcial; `reporte-proyecto.html` sigue con 55. Se actualizan
  al implementar, no en este spec.

## 9. Errores y avisos

- Tema inexistente → error claro, como ahora.
- `outfit_story` con base inexistente → error de datos.
- `outfit_story` sin `spicy1`/`spicy2`/`explicit1` → error de datos.
- Tema sin `outfit_story` en modo 5 stages → error salvo
  `--allow-legacy-outfit`; entonces warning y `outfit_fallback=true`.
- Pose↔escena incompatible → warning por defecto; exclusión con
  `--strict-scene`.
- `stage_cameras.json` ausente → fallback a `cameras.txt` con aviso.
- `pose_scene_affinity.yaml` ausente → sin validación, con aviso.
- LoRA de outfit ausente → warning; no se inventa el tag.

## 10. Verificación sin GPU

Todo esto se comprueba con `--dry-run`:

```bash
python Engine/pack_builder.py --char Makima_v2 --theme oficina --pack --dry-run
```

Criterios:

1. 45 jobs y **9 por stage**.
2. Un solo `outfit_base` y un solo `outfit_key` en todo el pack.
3. `sugerente` usa el base; `spicy1`/`spicy2`/`explicit1` usan base + mods;
   `explicit2` no lleva outfit.
4. Todas las cámaras pertenecen a `stage_cameras.json`.
5. `from side` no aparece como forzado en todas las poses laterales.
6. `pb0035`/`pb0036`/`pb0037` producen warning de pose↔escena en `oficina`.
7. `select_x_posts` no mete `spicy2` en SFW.
8. `pack_manifest` incluye `stage`, `outfit_base`, `outfit_key` y
   `outfit_fallback`; `metrics` incluye `by_stage`.

No se genera ninguna imagen para aprobar este spec.

## 11. Fuera de alcance

- LoRAs de ropa y sus pesos.
- Anclas automáticas de objetos (`on_bed`, `wall` inyectado).
- Cambiar el banco de poses o calibrar poses nuevas.
- Regenerar el pack de 45 actual.
- Full body como encuadre.
- Reentrenar o tocar `personajes.txt`.

## 12. Criterios de aceptación

- [ ] `nivel` y `stage` conviven sin romper packs viejos.
- [ ] 45 = 9/9/9/9/9 y el outfit base es único por pack.
- [ ] `spicy2` y `explicit1` tienen datos de outfit, no un `unbuttoned` reciclado.
- [ ] Cada stage tiene allowlist de cámara y `from side` no se fuerza.
- [ ] El mapa pose↔escena avisa de los tres casos conocidos.
- [ ] `select_x_posts` usa `stage` y no publica `spicy2` como SFW.
- [ ] El dry-run pasa sin GPU y sin escribir manifests de generación.
- [ ] La documentación operativa se actualiza después de implementar.
