Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
# PyPI name; the import package is `inline_core` (src/inline_core).
name = "inline-core"
version = "1.3.1"
version = "1.3.11"
description = "The generation engine behind Inline Studio."
readme = "README.md"
license = "GPL-3.0-or-later"
Expand Down
40 changes: 27 additions & 13 deletions core/src/inline_core/characters/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,27 @@ def __init__(
#: turned down and the character wire carries no controls of its own.
self.lora_strength = lora_strength

def prompt_prefix(self, first_position: int) -> str:
"""Text naming the positions the character lands on, so ordinal prompting resolves."""
def prompt_prefix(self, first_position: int, style: str = "ordinal") -> str:
"""Text naming the positions the character lands on, so positional prompting resolves.

``style`` because a model only resolves the form it was trained on: FLUX.2 reads the ordinal
prose below, MiniMax H3 reads ``<Picture N>`` tokens (``models/references.py``), and handing
either the other one names positions it cannot see.
"""
if not self.refs:
# A LoRA carries the likeness, so the description is all the prompt needs.
detail = " ".join(self.description.split())
return f"{detail} " if detail else ""
positions = [str(first_position + i) for i in range(len(self.refs))]
if len(positions) == 1:
positions = [first_position + i for i in range(len(self.refs))]
if style == "token":
tokens = " ".join(f"<Picture {n}>" for n in positions)
plural = "" if len(positions) == 1 else "each"
which = f"{tokens} {'shows' if not plural else 'show'}"
elif len(positions) == 1:
which = f"Image {positions[0]} shows"
else:
which = f"Images {', '.join(positions[:-1])} and {positions[-1]} show"
ordinals = [str(n) for n in positions]
which = f"Images {', '.join(ordinals[:-1])} and {ordinals[-1]} show"
line = f"{which} {self.name}, the same character in every image."
detail = " ".join(self.description.split())
if not detail:
Expand All @@ -67,7 +77,9 @@ def _cache_root() -> Path:
return data_dir() / "characters"


def char_apply(chosen: str, arch: str = encode.FLUX2_KLEIN_ARCH) -> AppliedCharacter | None:
def char_apply(
chosen: str, arch: str = encode.FLUX2_KLEIN_ARCH, prefer: str | None = None
) -> AppliedCharacter | None:
"""How a character applies on ``arch``, or None when none is picked. An unreadable pick raises
rather than silently generating the wrong person.

Expand All @@ -84,18 +96,20 @@ def char_apply(chosen: str, arch: str = encode.FLUX2_KLEIN_ARCH) -> AppliedChara

doc = cf.read(path)
digest = library.content_hash(path)
references = arch == encode.FLUX2_KLEIN_ARCH
# Whether this arch reads references at all, which is exactly the archs with a policy for them.
references = arch in encode.REFERENCE_POLICIES

if references and not cf.payload_valid(doc.manifest, arch, encode.PAYLOAD_ENCODER_VERSION):
doc = _recompile(doc, path)
doc = _recompile(doc, path, arch)
digest = library.content_hash(path)

description = _description(doc)
lora = _extract_lora(doc, digest, arch)
strength = encode.lora_strength(doc.manifest, arch)
# A trained adapter wins unless the character says otherwise: the user asked for it explicitly,
# and loading both would apply the identity twice.
mode = doc.manifest.apply.get(arch) or ("lora" if lora else "reference")
# and loading both would apply the identity twice. `prefer` overrides both, for a node that can
# only run one way: H3's reference partition needs a reference and cannot use an adapter alone.
mode = prefer or doc.manifest.apply.get(arch) or ("lora" if lora else "reference")
# No reference channel on this arch, so the adapter is the only way it can apply at all.
if not references:
mode = "lora"
Expand Down Expand Up @@ -137,9 +151,9 @@ def _description(doc: cf.CharDoc) -> str:
return raw.decode("utf-8", errors="replace") if raw else ""


def _recompile(doc: cf.CharDoc, path: Path) -> cf.CharDoc:
def _recompile(doc: cf.CharDoc, path: Path, arch: str = encode.FLUX2_KLEIN_ARCH) -> cf.CharDoc:
"""Rebuild a stale payload from ``refs/``. Payloads are cache, so this always works."""
logger.info("Recompiling the %s payload for %s", encode.FLUX2_KLEIN_ARCH, path.name)
logger.info("Recompiling the %s payload for %s", arch, path.name)
import io

from PIL import Image
Expand All @@ -153,7 +167,7 @@ def _recompile(doc: cf.CharDoc, path: Path) -> cf.CharDoc:
)
images.append(Image.open(io.BytesIO(raw)).convert("RGB"))

encode.build_payload(doc.manifest, doc.members, images)
encode.build_payload(doc.manifest, doc.members, images, arch)
cf.write(path, doc)
return doc

Expand Down
33 changes: 31 additions & 2 deletions core/src/inline_core/characters/charfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@
)


#: Where a reference came from. Nested inside `refs` rather than a new top-level key on purpose:
#: `from_json` models only the keys it knows, so a top-level addition is destroyed by any older
#: build that rewrites the file, and three code paths rewrite one routinely.
ORIGIN_ORIGINAL = "original"
ORIGIN_HARVESTED = "harvested"


def origin_of(ref: dict[str, Any]) -> str:
"""Absent means original: every reference written before harvesting existed is one."""
return str(ref.get("origin") or ORIGIN_ORIGINAL)


class CharFileError(Exception):
"""A ``.char`` that cannot be trusted. The message is shown to the user."""

Expand Down Expand Up @@ -190,12 +202,29 @@ def write(path: Path | str, doc: CharDoc) -> Path:


def refs_fingerprint(manifest: Manifest, policy: dict[str, Any]) -> str:
"""Ordered ref hashes plus policy, so a reorder or a resize-budget change both invalidate."""
parts = [str(ref.get("sha256", "")) for ref in manifest.refs]
"""Ordered ref hashes plus policy, so a reorder or a resize-budget change both invalidate.

Originals only. A harvested reference in here would mark the trained adapter stale the moment
one was taken, and `_extract_lora` drops a stale adapter with an INFO log - so the loop whose
whole point is a better adapter would silently switch off the adapter the user already has.
"""
kept = [ref for ref in manifest.refs if origin_of(ref) == ORIGIN_ORIGINAL]
parts = [str(ref.get("sha256", "")) for ref in kept]
parts.append(json.dumps(policy, sort_keys=True, separators=(",", ":")))
return hashlib.sha256("\x1f".join(parts).encode()).hexdigest()


def refs_identity(manifest: Manifest) -> str:
"""Which reference set this is, order included and policy excluded.

Separate from `refs_fingerprint`, which answers "is this payload still valid" and so covers
only the originals and the policy. This answers "were two nodes handed the same character",
which a payload node needs because it compiles from a doc it is not the one holding.
"""
parts = [f"{ref.get('sha256', '')}:{origin_of(ref)}" for ref in manifest.refs]
return hashlib.sha256("\x1f".join(parts).encode()).hexdigest()


def payload_valid(manifest: Manifest, arch: str, encoder_version: str) -> bool:
"""Whether ``payloads/<arch>/`` can be used as-is, or must be recompiled from ``refs/``."""
payload = manifest.payloads.get(arch)
Expand Down
Loading
Loading