diff --git a/docs/now/2026-09-13-crm-lead-magnet-the-clients-photo-as-a-free-9-16-gift-portrait.md b/docs/now/2026-09-13-crm-lead-magnet-the-clients-photo-as-a-free-9-16-gift-portrait.md new file mode 100644 index 000000000..435f5b0b9 --- /dev/null +++ b/docs/now/2026-09-13-crm-lead-magnet-the-clients-photo-as-a-free-9-16-gift-portrait.md @@ -0,0 +1,11 @@ +# NOW -- CRM lead magnet: the client's photo as a free 9:16 gift portrait (2026-09-13) + +## CRM lead magnet: the client's photo as a free 9:16 gift portrait (Closes #3597) + +- `specs/automation/crm-lead-magnet.t27` (module `automation::crm_lead_magnet`, `KIND = "automation"`) states what the CRM tool `crm_deliver_photo` of 999-multibots-telegraf gives away by default: the person's own Telegram profile photo redrawn (img2img) as a vertical 9:16 story portrait, `SOURCE = "lead-avatar"`, `FALLBACK = "text-to-image"` when no photo is readable and the tool says so; `TEXT_IN_IMAGE = false`, the caption carries the words. Safe-zone bands of the 1080x1920 story frame (top 14%, bottom 35%, sides 6%) keep the face in the central third. +- Who pays: the OWNER, op `gpt_image_edit` = 12 tokens (`ceil(0.03 * 2.0 / 0.005)`), `RECIPIENT_CHARGED = false`, `CHARGE_DEFERRED = false`. Model `gpt-image-2-5-flare-image-to-image` on Kie at `RESOLUTION = "1K"`, field `input_urls` (nano-banana-edit takes `image_urls`; the input shape is chosen by model, `editInputFor`). `COST_USD = "0.03"` is the public Kie price list read 2026-09-13. +- Prompt order `identity,scene,framing`: the "same person" constraint goes first, as what to KEEP; the owner's scene second; the frame last. Caption: one line, the person's name, names the assistant as the maker, exactly one question, no price, no link; `GIFTS_PER_PERSON = 1`. Nothing leaves without the owner's button; the touch is `written` with the note prefix `gift`. +- Keyboard width rule `MAX_BUTTONS_PER_ROW = 2`: Telegram shares a row's width between its buttons and three Russian labels with an emoji are cut on a phone. The host passes every CRM keyboard through `fit()` and ratchets it in a test. +- Checked: `public/t27/t27_compiler.wasm` through trinity `agents-from-specs.mjs` reports `typecheckOk: true, errors: 0, discarded: 0, hirOk: true`; ASCII only. Host PR gHashTag/999-multibots-telegraf#2358: render tests dm-delivery (27), kie-input-shape (6), the four price ratchets, tg-proposals (64); bot tests crmMenu (26, incl. the width ratchet), crmPlan, crmProactive -- green locally. +- Not claimed: `LIVE_RUN_STATUS = "not-yet-run"` -- no GPT Image 2.5 call has been made from this code base, and GramJS `downloadProfilePhoto` through the owner's session is by its types, not by a production run. +- `tools/published_figures.py`: the pins follow the corpus -- `pub const OP_*` 60 -> 61 (`OP_TOKENS`), test blocks 12719 -> 12724 (five in this spec); `--check` and `--self-check` pass on this checkout. diff --git a/specs/automation/crm-lead-magnet.t27 b/specs/automation/crm-lead-magnet.t27 new file mode 100644 index 000000000..390a32533 --- /dev/null +++ b/specs/automation/crm-lead-magnet.t27 @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/automation/crm-lead-magnet.t27 -- the CRM lead magnet: a person's own Telegram +// photo redrawn as a vertical 9:16 story portrait and given away free, to show the AI +// assistant doing something for THEM before anything is sold. +// Host: 999-multibots-telegraf apps/vibee-editor/render/src/agent/crm-deliver-tool.ts +// (the tool crm_deliver_photo, gift mode by default), src/kie-image.ts (editInputFor), +// src/agent/tools.ts (leadAvatarUrl: owner session first, bot API second), +// src/agent/billing-shared.ts (the op gpt_image_edit and its price). +// Consumer: the bot src/navigation/helpers/crmMenu.ts (keyboard width rule, prep label). +// Why a personal artefact: a generic lead magnet is content the person has seen before; a +// picture WITH THEM in it is opened, kept and forwarded. This spec fixes what the gift is, +// who pays for it, how the prompt is ordered so the face stays theirs, and how wide a +// keyboard row may be so the buttons that deliver it can be read on a phone. +// Claim status: the Kie GPT Image 2.5 contract and price are DOCUMENTED (docs.kie.ai and the +// public price list, both read 2026-09-13), NOT yet live-probed from this code base. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module automation::crm_lead_magnet { + + pub const KIND : str = "automation"; + pub const ID : str = "crm-lead-magnet"; + pub const REPO : str = "999-multibots-telegraf"; + pub const VERSION : u8 = 1; + + // ---- What is made --------------------------------------------------------------------- + pub const TOOL : str = "crm_deliver_photo"; + pub const DEFAULT_MODE : str = "gift"; // gift=true unless the owner says gift=false + pub const SOURCE : str = "lead-avatar"; // the person's own profile photo, img2img + pub const FALLBACK : str = "text-to-image"; // when no photo is readable; said in `source` + pub const ASPECT_RATIO : str = "9:16"; + pub const STORY_PX : str = "1080x1920"; + pub const TEXT_IN_IMAGE : bool = false; // the caption carries the words, never the pixels + + // The story frame is covered by app UI. Nothing important lives in these bands, and the + // face sits in the central third of the frame. + pub const SAFE_ZONE_TOP_PCT : u8 = 14; + pub const SAFE_ZONE_BOTTOM_PCT : u8 = 35; + pub const SAFE_ZONE_SIDE_PCT : u8 = 6; + + // ---- Which model, and what it costs ----------------------------------------------- + pub const MODEL : str = "gpt-image-2-5-flare-image-to-image"; // Kie; sunburst = premium tier + pub const MODEL_ALT : str = "gpt-image-2-5-sunburst-image-to-image"; + pub const RESOLUTION : str = "1K"; + pub const KIE_INPUT_URLS_FIELD : str = "input_urls"; // nano-banana-edit uses image_urls + pub const COST_USD : str = "0.03"; // Kie price list, 1K, read 2026-09-13 + pub const OP : str = "gpt_image_edit"; // the billing op the OWNER is charged + pub const OP_TOKENS : u8 = 12; // ceil(0.03 * 2.0 / 0.005) + pub const PAYER : str = "owner"; + pub const RECIPIENT_CHARGED : bool = false; + pub const CHARGE_DEFERRED : bool = false; // the owner pays at generation, not at the press + pub const LIVE_RUN_STATUS : str = "not-yet-run"; // documented + priced, not probed + + // ---- The prompt order ------------------------------------------------------------- + // Identity FIRST, the owner's scene second, framing last. img2img models weigh the opening + // of the prompt most, so "the same person" goes before the scene, phrased as what to KEEP. + pub const PROMPT_ORDER : str = "identity,scene,framing"; + pub const IDENTITY_FIRST : bool = true; + + // ---- The caption ------------------------------------------------------------------ + pub const CAPTION_ONE_LINE : bool = true; + pub const CAPTION_HAS_NAME : bool = true; + pub const CAPTION_NAMES_MAKER : bool = true; // "my AI assistant made this" + pub const CAPTION_QUESTIONS : u8 = 1; // one soft question, the CTA + pub const CAPTION_HAS_PRICE : bool = false; + pub const CAPTION_HAS_LINK : bool = false; + pub const GIFTS_PER_PERSON : u8 = 1; // the second portrait is a paid service + + // ---- Nothing leaves without the owner's press -------------------------------------- + pub const SENT_WITHOUT_BUTTON : bool = false; + pub const TOUCH_KIND : str = "written"; // a gift is a touch, not a purchase + pub const TOUCH_NOTE_PREFIX : str = "gift"; // SELLER_NOTE_PREFIXES.gift in crm-notes.ts + + // ---- The keyboard width rule ----------------------------------------------------- + // Telegram shares a row's width between its buttons; three Russian labels with an emoji + // are cut on a phone, two are readable. Every CRM keyboard passes through `fit`. + pub const MAX_BUTTONS_PER_ROW : u8 = 2; + pub const HUB_ROWS : u8 = 2; // list + overview, then sweep + pub const PREP_LABEL_DELIVER : str = "gift photo 9:16"; + + struct Gift { + source: str, // SOURCE | FALLBACK + aspect_ratio: str, + model: str, + payer: str, + recipient_charged: bool, + text_in_image: bool, + } + + struct Caption { + one_line: bool, + has_name: bool, + names_maker: bool, + questions: u8, + has_price: bool, + has_link: bool, + } + + struct KeyboardRow { + buttons: u8, + } + + test "the gift is drawn from the person's own photo at 9:16 and the recipient pays nothing" + const g = Gift { source: "lead-avatar", aspect_ratio: "9:16", model: "gpt-image-2-5-flare-image-to-image", payer: "owner", recipient_charged: false, text_in_image: false }; + assert(g.source == SOURCE); + assert(g.aspect_ratio == ASPECT_RATIO); + assert(g.model == MODEL); + assert(g.payer == PAYER); + assert(!g.recipient_charged); + assert(!g.text_in_image); + + test "no readable photo falls back to text-to-image, still 9:16, still a gift" + const g = Gift { source: "text-to-image", aspect_ratio: "9:16", model: "image_generate", payer: "owner", recipient_charged: false, text_in_image: false }; + assert(g.source == FALLBACK); + assert(g.aspect_ratio == ASPECT_RATIO); + assert(!g.recipient_charged); + + test "the caption names the person and the maker, asks once, sells nothing" + const c = Caption { one_line: true, has_name: true, names_maker: true, questions: 1, has_price: false, has_link: false }; + assert(c.one_line); + assert(c.has_name); + assert(c.names_maker); + assert(c.questions == CAPTION_QUESTIONS); + assert(!c.has_price); + assert(!c.has_link); + + test "a row of three is split; a row of two stands" + const wide = KeyboardRow { buttons: 3 }; + const fine = KeyboardRow { buttons: 2 }; + assert(wide.buttons > MAX_BUTTONS_PER_ROW); + assert(fine.buttons <= MAX_BUTTONS_PER_ROW); + + test "the price is the documented Kie 1K price with the platform markup" + assert(OP_TOKENS == 12); + assert(LIVE_RUN_STATUS == "not-yet-run"); +} diff --git a/tools/published_figures.py b/tools/published_figures.py index 38211cfb2..6fe490c79 100755 --- a/tools/published_figures.py +++ b/tools/published_figures.py @@ -59,7 +59,8 @@ # #3567 (S05) added the forty-seven opcodes of specs/isa/ternary_encoding.t27 and OP_NOP, OP_HALT # of specs/vm/trinity_vm.t27: 11 + 49 = 60. ("pub const OP_* declarations", "declarations", - r"^\s*pub\s+const\s+OP_\w+", 60, "#3497 said 20 -- that was a SITE count in the C"), + # #3596 added specs/automation/crm-lead-magnet.t27 with `pub const OP_TOKENS`: 60 + 1 = 61. + r"^\s*pub\s+const\s+OP_\w+", 61, "#3497 said 20 -- that was a SITE count in the C; #3596 added 1"), ("abs( uses", "uses", r"(?