cutan.characters.factory¶
High-level entry points: build and inspect a character.
The new_character() function wires together fetching/wrapping art,
slicing it into per-part SVGs, generating the default mouth set, and
writing a complete character directory + character.json descriptor.
Checking one is cutan.characters.validate’s job, not this module’s — it
opens every part and reports an.verify._base.Finding s, so a character
problem routes the way every other verifier’s does (an#78).
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
... descriptor_path = new_character(d, name='nobody', use_dicebear=False)
... descriptor_path.parent.name, descriptor_path.name
('nobody', 'character.json')
Module Attributes
The roles |
|
The roles a head's own art carries. |
|
it is the drawing's ink, not a costume colour. |
|
The shoe, drawn in the leg part. |
|
The pupil, in its own part (or the pre-gaze open eye). |
|
Default hand, trouser and brow colours — the literals the factory always drew. |
|
The hats |
|
The |
|
|
|
|
|
The largest head scale accepted — past it the head no longer fits the 1024-unit view box above a regular body. |
|
Named builds. |
|
every drawing call that made the character, in order, with its parameters — so the factory's bytes can be re-derived anywhere ( |
|
The version of the recipe's format. |
|
The provider of every per-part source the factory stamps on what it draws. |
|
The eye's geometry in its 64x32 canvas, shared by the four synthesizers so the sclera, the pupil and the lid outline agree (an#99). |
|
How much farther than the eye white the FILLED closed lid reaches, in eye view-box units (cutan#66). |
|
The parts a rig gains with |
|
The face slots of the default rig with the eye stack (an#99). |
|
how far the near eye, its stack and brow slide toward the face's edge, and the mouth with them (view_box units at head_scale 1); the mouth is narrowed, seen edge-on. |
|
both hang from under the body, the near leg ( |
|
the whole face slides toward the facing side, the far eye narrows, the far arm tucks in toward the body and the legs in. |
|
The most drawing steps a recipe may replay, and the most bytes of JSON its parameters may take: the replay runs on whoever reads the descriptor, so its cost is bounded here, not by the recipe's author (review-292 R3). |
Functions
|
Give a character the eye stack (an#99): three sibling slots per eye under the head — |
|
Give a factory character its turnaround (an#197): |
|
Whether |
|
Declare a |
|
What covers a FACTORY head's brows, derived from its recorded knobs (an#284, ADR 0002 decision 2): its hat, as drawn ( |
|
The descriptor-level source of a character this factory drew, pinned to its drawing. |
|
The per-part source of a part this factory drew, pinned to its bytes. |
|
The pupil's travel per axis, in view-box units: the sclera's clearance minus the pupil's radius — the semi-axes of the inner ellipse the gaze axes' unit circle maps onto. |
|
Build a complete character on disk. |
|
Log what the body writes, then record the factory-stamped bytes it wrote at |
|
|
|
Rewrite each part SVG's root size by |
|
How far a character's art reaches above and below its stage point, in scene pixels at |
|
Record the factory as the source of the character it just drew at |
|
Give each part the factory drew a |
|
Pin a generator's |
|
Convert an existing character to stand on its feet (an#285), or back. |
|
|
Classes
|
The proportions of a synthesized body, in view_box units. |
- cutan.characters.factory.BUILDS: dict[str, BodyBuild] = {'regular': BodyBuild(torso_size=(256, 256), torso_radius=40, torso_inset_bottom=20, arm_width=36, arm_length=256, hand_radius=20, limb_stroke=4, leg_width=40, leg_length=300.0, shoe_size=(32, 18), shoulder=(90, 240), hip_x=50, neck_height=260), 'squat': BodyBuild(torso_size=(300, 220), torso_radius=80, torso_inset_bottom=4, arm_width=36, arm_length=140, hand_radius=18, limb_stroke=4, leg_width=50, leg_length=96, shoe_size=(34, 16), shoulder=(118, 168), hip_x=46, neck_height=214), 'stick': BodyBuild(torso_size=(170, 210), torso_radius=14, torso_inset_bottom=4, arm_width=10, arm_length=200, hand_radius=9, limb_stroke=3, leg_width=10, leg_length=230, shoe_size=(15, 7), shoulder=(82, 188), hip_x=28, neck_height=214), 'tall': BodyBuild(torso_size=(224, 320), torso_radius=36, torso_inset_bottom=4, arm_width=32, arm_length=320, hand_radius=18, limb_stroke=4, leg_width=36, leg_length=380, shoe_size=(30, 16), shoulder=(80, 304), hip_x=44, neck_height=324)}¶
Named builds.
regularis today’s body, number for number.
- class cutan.characters.factory.BodyBuild(torso_size=(256, 256), torso_radius=40, torso_inset_bottom=20, arm_width=36, arm_length=256, hand_radius=20, limb_stroke=4, leg_width=40, leg_length=300.0, shoe_size=(32, 18), shoulder=(90, 240), hip_x=50, neck_height=260)[source]¶
Bases:
objectThe proportions of a synthesized body, in view_box units.
Every length that decides where a part hangs is here, so the bones the factory writes and the art it draws are derived from ONE record and cannot disagree — a leg’s art is exactly
leg_lengthtall and its bone sitsleg_lengthabove the ground, so it hangs from the hip to the ground at every build (tests/test_rig_layout.py).- shoulder: tuple[float, float] = (90, 240)¶
The shoulder joint (x from the centre line, height above the hip).
- cutan.characters.factory.DFLT_HAND_COLOUR: str = '#f1c9a5'¶
Default hand, trouser and brow colours — the literals the factory always drew.
- cutan.characters.factory.EYE_CANVAS: tuple[int, int] = (64, 32)¶
The eye’s geometry in its 64x32 canvas, shared by the four synthesizers so the sclera, the pupil and the lid outline agree (an#99).
- cutan.characters.factory.FACE_SLOTS: tuple[str, ...] = ('left_eye', 'right_eye', 'left_sclera', 'right_sclera', 'left_pupil', 'right_pupil', 'mouth', 'left_brow', 'right_brow')¶
The face slots of the default rig with the eye stack (an#99).
- cutan.characters.factory.FACTORY_ART_PROVENANCE: str = 'fallback_geometric'¶
The
art_provenanceof a head the factory drew (its knobs re-draw it).
- cutan.characters.factory.FACTORY_AUTHOR: str = 'an (generated locally)'¶
The provider of every per-part source the factory stamps on what it draws. The licence of the factory’s own drawings: no rights to clear. Who the factory’s descriptor-level source names as the author.
- cutan.characters.factory.GAZE_PARTS: tuple[str, ...] = ('sclera_l', 'sclera_r', 'pupil_l', 'pupil_r')¶
The parts a rig gains with
an character add-gaze. Optional — never inREQUIRED_PARTS: a pre-Wave-6 rig without them still renders, and gaze is a no-op on it.
- cutan.characters.factory.HAIR_LENGTHS: tuple[str, ...] = ('short', 'medium', 'long')¶
short(nothing below the crown — the default),medium(falling beside the face to the jaw) andlong(past the chin, in locks that keep clear of the neck and the collar).- Type:
Hair lengths
- cutan.characters.factory.HAIR_STYLES: tuple[str, ...] = ('peak', 'bald', 'bun', 'curly')¶
peak(the factory’s original hair, a widow’s peak — the default),bald,bun(the hair gathered in a bun on the crown) andcurly(a halo of curls around the crown). Every style keeps the default hairline over the forehead — none draws lower over the brows (tests) — so a hair style never costs brow acting.- Type:
Hair styles
- cutan.characters.factory.HATS: tuple[str, ...] = ('none', 'cap', 'beanie', 'bowler', 'bicorne')¶
The hats
new_character()can draw.
- cutan.characters.factory.HEAD_ART_ROLES: frozenset[str] = frozenset({'hair', 'skin'})¶
The roles a head’s own art carries. On a head the factory did not draw (DiceBear) they are left untagged everywhere, never half-tagged.
- cutan.characters.factory.LID_COVER_PAD: int = 3¶
How much farther than the eye white the FILLED closed lid reaches, in eye view-box units (cutan#66). A lid exactly the sclera’s ellipse leaves the white’s anti-aliased edge showing as a light ring round a closed eye: measured at a 720p silhouette’s eye size (0.34 px a unit), a grey of 63 on black with no pad, 4 with 2 units, none with 3.
- cutan.characters.factory.MAX_HEAD_SCALE: float = 2.5¶
The largest head scale accepted — past it the head no longer fits the 1024-unit view box above a regular body.
- cutan.characters.factory.MAX_RECIPE_STEPS: int = 8¶
The most drawing steps a recipe may replay, and the most bytes of JSON its parameters may take: the replay runs on whoever reads the descriptor, so its cost is bounded here, not by the recipe’s author (review-292 R3).
- cutan.characters.factory.OUTLINE_COLOUR: str = '#222222'¶
it is the drawing’s ink, not a costume colour.
- Type:
The outline every synthesized body part is stroked in. Untagged
- cutan.characters.factory.PALETTE_ROLES: tuple[str, ...] = ('skin', 'hair', 'clothing', 'leg', 'accessory')¶
The roles
new_character(palette=...)takes. They areStylePackrole names on purpose: a palette chosen at authoring time and a pack applied at compile time speak one vocabulary, and the factory records each as a colour role so the pack can reach what the palette drew.
- cutan.characters.factory.PUPIL_COLOUR: str = '#1a1a1a'¶
The pupil, in its own part (or the pre-gaze open eye). Role
pupil.
- cutan.characters.factory.RECIPE_KEY: str = 'factory'¶
every drawing call that made the character, in order, with its parameters — so the factory’s bytes can be re-derived anywhere (
redraw_digests()).- Type:
The descriptor
metadatakey holding the factory’s recipe (an#292)
- cutan.characters.factory.SIDE_EYE_SHIFT: float = 14.0¶
how far the near eye, its stack and brow slide toward the face’s edge, and the mouth with them (view_box units at head_scale 1); the mouth is narrowed, seen edge-on.
- Type:
Profile (facing right)
- cutan.characters.factory.SIDE_LEG_OFFSET: float = 0.25¶
both hang from under the body, the near leg (
leg_r, drawn over the far one) a little forward and the far leg a little back, overlapping at the hip — each hip sitsSIDE_LEG_OFFSETleg widths off the centre line — and splayed so the FEET part: the shoe centres landSIDE_FOOT_SPREADleg widths apart, on every build (a stubby leg splays more). Both legs show, even as one silhouette, and a walk in profile has two legs to alternate (an#203). A stick leg is thinner than its shoe, so leg widths alone would leave the two shoes on top of each other — a one-legged stand; the shoes part by at leastSIDE_FOOT_MIN_SHOESshoe lengths (a floor every other build already clears, so they are unchanged).- Type:
Profile legs (facing right)
- cutan.characters.factory.THREE_QUARTER_FACE_SHIFT: float = 20.0¶
the whole face slides toward the facing side, the far eye narrows, the far arm tucks in toward the body and the legs in.
- Type:
Three-quarter (facing right)
- cutan.characters.factory.add_gaze(char_dir, *, skin=None, overwrite_eyes=False)[source]¶
Give a character the eye stack (an#99): three sibling slots per eye under the head —
<side>_sclera(white fill) below<side>_pupilbelow<side>_eye(the existing slot, now the lid, drawn above the pupil) — with synthesized parts, an outline-only open eye, a FILLED closed lid, thegaze_travelclamp, and draw orders that put the lid over the pupil. Idempotent: a rig that already has the stack is rewritten to the same state. Returns the descriptor path.The open and closed eye parts are REWRITTEN (outline-only, filled lid), so on a rig whose eyes are not this factory’s drawings — a promoted hand rig — it refuses unless
overwrite_eyes=True: the stack’s geometry is the synthesized eye’s, and an illustrator’s eyes would be silently replaced (an#99 review). Such a rig wants its own outline-only open eye, filled lid, sclera and pupil parts drawn to its own geometry.This is the expand step for a pre-Wave-6 descriptor: no migration inserts pupil slots, because their art would be absent and absent art is fatal under
strict_assets— every existing character would stop rendering on the bench.- Return type:
- cutan.characters.factory.add_views(char_dir)[source]¶
Give a factory character its turnaround (an#197):
back,sideandthree_quarterhead and torso art beside the front, aviewswap set projected onto those two slots, and a pose per view (swap_poses) — so{kind: set, target: <entity>, property: view, value: side}orcutan.motion.turn()turns the whole character. Idempotent. Returns the descriptor path.The views are REDRAWN from the recorded knobs (seed, palette, build, hat, hair style and length, sash, head scale), so it refuses a rig whose head is not this factory’s drawing for them — a DiceBear head (its face is baked, and there is no back of it to draw), a promoted hand rig, or an edited head: its views are an illustrator’s to draw, declared the same way (a
viewset whose keys name attachments on the head and torso slots, andswap_poses).Every colour is role-tagged like the front’s, so a StylePack recolours the views exactly as it recolours the front (an#191). The existing art is not touched: a shot that never sets a view renders byte-identically.
- Return type:
- cutan.characters.factory.brow_cover_unknowable(desc)[source]¶
Whether
descis a factory head with a hat whose drawing is not the factory’s for its recorded knobs (edited by hand), so its brow cover cannot be derived: only a declaredoccludedcan say (an#284).- Return type:
- cutan.characters.factory.declare_mouth_variants(descriptor, variants)[source]¶
Declare a
viseme@<form>set per variant ondescriptor— the set’s keys map tomouth_<shape>_<form>attachments, which are added to the default skin’smouthslot with the neutral mouth’s geometry. The neutral set is the SSOT for which shapes exist; a variant mirrors it.- Return type:
- cutan.characters.factory.derived_brow_cover(desc)[source]¶
What covers a FACTORY head’s brows, derived from its recorded knobs (an#284, ADR 0002 decision 2): its hat, as drawn (
_worn_seat()), measured against the brows’ acting range (seat_overlap()).Nonewhen nothing does, when the head is not the factory’s (drawn art: an illustrator declaresoccluded), or when it cannot be told (an edited factory head).Derived, never stored: a character made before hats were seated (an#252) reports the brim that covers its brows, and the answer follows the measurement if the brows’ range moves.
- cutan.characters.factory.factory_descriptor_source(source_svg)[source]¶
The descriptor-level source of a character this factory drew, pinned to its drawing.
The digest is that of the descriptor’s
source_svg— the drawing every part was cut from. Like a part stamp it speaks only for bytes it pins: the asset library andan creditsread every file of the character that no stamp pins (a part re-carved later, a file added by hand) as UNVERIFIED, never as the factory’s (an.credits._part_credits()).>>> s = factory_descriptor_source(b"<svg/>") >>> (s.provider, s.license, len(s.sha256)) ('an character factory', 'cc0-1.0', 64)- Return type:
AssetSource
- cutan.characters.factory.factory_source(data)[source]¶
The per-part source of a part this factory drew, pinned to its bytes.
>>> factory_source(b"<svg/>").license, len(factory_source(b"<svg/>").sha256) ('cc0-1.0', 64)- Return type:
AssetSource
- cutan.characters.factory.gaze_travel_for(rx=14, ry=10, pupil_r=5)[source]¶
The pupil’s travel per axis, in view-box units: the sclera’s clearance minus the pupil’s radius — the semi-axes of the inner ellipse the gaze axes’ unit circle maps onto. The compiler clamps the summed gaze to 0.95 of that circle (
GAZE_ELLIPSE_MARGIN), which is what keeps the pupil disc inside the white at every angle without a runtime mask.>>> gaze_travel_for() {'x': 9.0, 'y': 5.0}
- cutan.characters.factory.new_character(out_dir, *, name, seed=None, style='lorelei', voice_ref=None, use_dicebear=True, acknowledge_attribution=False, overwrite=False, mouth_variants=None, gaze=True, palette=None, build='regular', head_scale=1.0, hat='none', sash=False, views=True, hair_style='peak', hair_length='short', feet_origin=False)[source]¶
Build a complete character on disk.
Variety knobs (every default reproduces the pre-knob character byte for byte, which a golden test holds):
palette—{role: "#rrggbb"}overPALETTE_ROLES(skin,hair,clothing,leg,accessory) — the SAME role names aStylePackuses. Unset roles keep the seed’s colours.skinalso paints the hands and the gaze lid;hairthe brows and the collar. On a DiceBear head only the body follows (its face is baked).build— a key ofBUILDS:regular,squat(round body, short legs),tall,stick(small blocky body, stick limbs).head_scale— the head and its whole face (eyes, brows, mouths, their offsets, the pupil travel) scaled together, so a big head keeps its face.hat— a key ofHATS(offline head only), inaccessory, worn above the brows’ acting range at this head scale (an#252): lifted, and flattened toward its crown when lifting is not enough. A hat that still covers the brows (a very small head) is recorded in the descriptor’soccluded, so the character does not affordface.browsand expressions fall to the lids, gaze and mouth.hair_style—HAIR_STYLES:peak(the default),bald,bun,curly;hair_length—HAIR_LENGTHS:short(the default),medium,long(offline head only). Drawn in thehairrole (the palette’shaircolours them), in every view.sash— a diagonal band across the torso, inaccessory.views(an#197) — draw the turnaround:back,side(a profile facing the viewer’s right) andthree_quarterbeside the front, as aviewswap set with a pose per view (add_views()), socutan.motion.turn()can turn the character around. Offline head only(a DiceBear face is baked into its art); ignored for a DiceBear head.
Additive: a shot that never sets a view renders exactly as without it.
feet_origin(an#285) — declare the rig’soriginat its ROOT bone, the ground contact, sostage.atis where the feet stand and every build placed at oneystands on one line (stage_extent’sfeetis then 0).an character newturns it on by default (the maintainer’s decision on an#423, 2026-10-09); here it stays off by default, so code that rebuilds a character (goldens, a recipe’s replay, the demos) draws exactly what it drew.stand_on_feet()converts an existing character, one at a time.
Every colour the factory draws in a role is recorded in the descriptor’s
colour_rolesso a style pack can recolour it later (palette swapping,cutan.characters.colour_roles).gaze(an#99) adds the eye stack — sclera and pupil slots under each lid, a filled closed lid, thegaze_travelclamp — throughadd_gaze(), sogaze_x/gaze_yand the ambient saccades reach the pupils. Off, the eye is the single pre-stack drawing.mouth_variants(an#98) —{form: smile offset}— writes one more 9-shape mouth set per form (mouth_<shape>_<form>.svg) and declares it as theviseme@<form>swap set, with its attachments in the default skin’smouthslot, so an expression preset preferring that form selects it.NonemeansDEFAULT_MOUTH_VARIANTS(happy, sad);{}means the neutral set only.Steps:
Fetch a DiceBear avatar (skip if
use_dicebear=False— useful for offline tests).Wrap it into the canonical
ancutout SVG (skeleton + illustration groups), saved as<name>.svg.Slice each part into
parts/<part>.svg.Write the 9-shape default mouth set into
parts/mouth/.Synthesize a few derived parts (open/closed eyes, brows) so the character is complete out of the box.
Emit a
character.jsondescriptor.
Returns the path to the created
character.json.Raises
FileExistsErrorifout_dir/namealready exists andoverwrite=False.- Return type:
- cutan.characters.factory.recording_drawn(char_dir)[source]¶
Log what the body writes, then record the factory-stamped bytes it wrote at
char_dir.For a drawing path outside this module (
an character mouths): only bytes written throughcutan.characters.drawninside the block, and stamped by the factory, are recorded.
- cutan.characters.factory.redraw_digests(descriptor)[source]¶
{path: sha256}of every file the factory draws fromdescriptor’s recipe (an#292).The factory is deterministic: replaying the recorded drawing calls, with their recorded parameters, into a scratch folder re-derives the very bytes — anywhere, on any machine. Whatever bytes it draws are the factory’s own work, so a recipe can only ever confirm the factory’s output: carved or hand-drawn bytes are never what it draws.
{}when the descriptor records no replayable recipe (none, an older format, a DiceBear head, a parameter JSON could not hold). Memoised per recipe.
- cutan.characters.factory.scale_part_files(paths, scale)[source]¶
Rewrite each part SVG’s root size by
scale(its drawing untouched): the compiler draws a part at its own raster size, so that IS its size on screen. Missing files are skipped.- Return type:
- cutan.characters.factory.stage_extent(desc)[source]¶
How far a character’s art reaches above and below its stage point, in scene pixels at
stage.scale: 1:{"top", "feet", "height"}.The stage point (
stage.at) is the rig’s declaredoriginwhen it has one (an#338); otherwise it is not the feet: the compiler places a rig by the middle of its bones’ extent, between the neck and the feet, so where the feet land depends on the build and the head scale (a squat figure’s feet sit about half as far below the point as a tall one’s). Read from the compiler’s own placement rule (an.stage.rig.rig_origin) and the head’s art, so this is what the compiled scene does, not a second guess at it. Multiply bystage.scale. The head reaches its drawing’s top edge (a hat stays inside it).>>> e = stage_extent(CharacterDescriptor(name="c")) >>> round(e["top"]), round(e["feet"]) (169, 94)
- cutan.characters.factory.stamp_factory_descriptor(char_dir)[source]¶
Record the factory as the source of the character it just drew at
char_dir.Only a descriptor that declares no source is stamped (a DiceBear head carries DiceBear’s); the stamp pins the bytes of its
source_svg. Called bynew_character()on what it has just drawn, never on a character someone may have edited since.- Return type:
- cutan.characters.factory.stamp_factory_parts(char_dir, paths, *, skip=())[source]¶
Give each part the factory drew a
cc0per-part source pinned to its digest.Rights in the asset library attach to the BYTES (an#236): a file is as restricted as the strictest thing any library says about its SHA-256, and an asset-level licence speaks for every file the asset does not itemise. The factory’s parts are byte-identical across characters (the default mouths, the eyes), so without this stamp a carved character built on a factory body would make every other character’s shared parts private. The stamp pins the digest, so a part later re-drawn or re-carved no longer matches it and stops being itemised as the factory’s — the stamp cannot launder new bytes.
- paths: the relative paths this call has just DRAWN — required (an#249
R4-N1): a stamp says “the factory made these bytes”, so only the code that made them may write it. Stamping every part by default would label a part re-carved since as the factory’s
cc0. A part carrying some other provider’s source is never re-stamped.
- Return type:
- cutan.characters.factory.stamp_generated_head(char_dir, source)[source]¶
Pin a generator’s
source(DiceBear’s) to the bytes it produced atchar_dir.The head part(s) the generator drew (
parts/head.svg) carry the source with their own digest, and the descriptor carries it with the digest of itssource_svg. Like the factory’s stamps, it then speaks only for those bytes: a part re-carved since, or a file added, is UNVERIFIED inan creditsandunknownin the asset library. Called bynew_character()on what it has just written.- Return type:
- cutan.characters.factory.stand_on_feet(char_dir, *, undo=False)[source]¶
Convert an existing character to stand on its feet (an#285), or back.
Declares the rig’s
originat its ROOT bone, the ground contact, the wayan character newdoes for a new character:stage.atis then where its feet stand. Existing characters are never converted implicitly (that would move them in every scene already made), so this is the opt-in, one character at a time.undoremoves the declared origin, back to the middle of the bones.Only
character.json’soriginchanges; every other key is kept as written. Returns{"before", "after"}(stage_extent()of each) andshift: how many scene pixels (atstage.scale: 1) to ADD to the character’sstage.aty in a scene made before, to keep it where it stood (negative afterundo).
- cutan.characters.factory.view_poses(body=BodyBuild(torso_size=(256, 256), torso_radius=40, torso_inset_bottom=20, arm_width=36, arm_length=256, hand_radius=20, limb_stroke=4, leg_width=40, leg_length=300.0, shoe_size=(32, 18), shoulder=(90, 240), hip_x=50, neck_height=260), *, head_scale=1.0, slots=None)[source]¶
{view: {slot: SlotPose}}for the factory’s rig built asbody— what a view does besides swapping art: the back hides the face, the side hides the far eye and arm and slides the near eye and mouth to the profile edge.Face offsets scale with
head_scale(the face was drawn at it), limb offsets come from the build’s own joints.slotslimits the poses to the slots a rig has (a rig without the eye stack has no pupils to pose).>>> poses = view_poses() >>> sorted(poses) ['back', 'front', 'side', 'three_quarter'] >>> poses["front"], poses["back"]["mouth"].alpha, poses["side"]["arm_r"].x ({}, 0.0, -90.0)