Add a structured authenticity signal with a distinct banner, instead of burying it in card_note
The Charizard grade that prompted this had the model writing 'possible non-standard/proxy print' directly into card_note -- the only free-text field available -- since there was nowhere else for that observation to go. That's why it got no visual treatment: the UI renders card_note as a plain grey subtitle, identical to any ordinary card name. Added authenticity as its own top-level field (flag: none/worth_checking/ likely_not_genuine, plus an observation) alongside the four grading categories but explicitly separate from them -- this is about whether the card is a genuine product at all, not its condition, and PSA authenticates before it grades, so a flagged card's estimated_grade goes to null rather than a number. Prompted with concrete tells for both classes of concern: fan-made/proxy prints (fictional sets, non-existent number combos, home-printer texture, 'proxy' watermarks) and counterfeits of real cards (colour/font/holo mismatches against genuine copies). Trimming (already covered under CORNERS from an earlier fix) now also sets this field when flagged, so the UI has one place to check regardless of which specific issue triggered it. UI: a full banner (not a hint line) above the slab -- critical red for likely_not_genuine, warning amber for worth_checking -- plus a matching dot next to the card name in History so it's visible without opening the card. Both colours reuse --critical/--warning rather than the brand red, since this is exactly the 'distrust this' signal those are already reserved for. Migration tested against the live schema shape; existing rows get authenticity=None and render with no banner, as before.
This commit is contained in:
parent
d026cce91a
commit
2b3bbaf40f
5 changed files with 148 additions and 12 deletions
1
app.py
1
app.py
|
|
@ -453,6 +453,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||||
"closeups": result["closeups"],
|
"closeups": result["closeups"],
|
||||||
"card_type": result["card_type"],
|
"card_type": result["card_type"],
|
||||||
"card_note": result["card_note"],
|
"card_note": result["card_note"],
|
||||||
|
"authenticity": result["authenticity"],
|
||||||
"edge_measurements": result["edge_measurements"],
|
"edge_measurements": result["edge_measurements"],
|
||||||
"centering_measurement": result["centering_measurement"],
|
"centering_measurement": result["centering_measurement"],
|
||||||
"aspect_measurement": result["aspect_measurement"],
|
"aspect_measurement": result["aspect_measurement"],
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,16 @@ function renderGradeBlock(g, opts = {}) {
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
const auth = g.authenticity || null;
|
||||||
|
const authBanner = (auth && auth.flag !== 'none') ? `
|
||||||
|
<div class="authenticity-banner authenticity-${auth.flag === 'likely_not_genuine' ? 'high' : 'check'}">
|
||||||
|
<div class="authenticity-title">${auth.flag === 'likely_not_genuine'
|
||||||
|
? 'Possibly not a genuine card' : 'Worth double-checking'}</div>
|
||||||
|
${auth.observation ? `<div class="authenticity-detail">${esc(auth.observation)}</div>` : ''}
|
||||||
|
</div>` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
${authBanner}
|
||||||
<div class="note note-warn">A photo can't show surface scratches, print lines, or light edge wear the
|
<div class="note note-warn">A photo can't show surface scratches, print lines, or light edge wear the
|
||||||
way a grader's raking light does — and a seller's listing photo is often lit to hide them.
|
way a grader's raking light does — and a seller's listing photo is often lit to hide them.
|
||||||
Treat this as a rough screen, not a prediction of what it comes back as.</div>
|
Treat this as a rough screen, not a prediction of what it comes back as.</div>
|
||||||
|
|
@ -398,13 +407,21 @@ function renderHistory() {
|
||||||
const rows = state.history;
|
const rows = state.history;
|
||||||
$('#history-count').textContent = rows.length ? `${rows.length} graded` : '';
|
$('#history-count').textContent = rows.length ? `${rows.length} graded` : '';
|
||||||
$('#history-empty').hidden = rows.length > 0;
|
$('#history-empty').hidden = rows.length > 0;
|
||||||
$('#history-body').innerHTML = rows.map((g) => `
|
$('#history-body').innerHTML = rows.map((g) => {
|
||||||
|
const auth = g.authenticity;
|
||||||
|
// A dot rather than repeating the full banner text — the row is
|
||||||
|
// already tight, and opening the card gives the real explanation.
|
||||||
|
const authDot = (auth && auth.flag !== 'none')
|
||||||
|
? `<span class="authenticity-dot authenticity-${auth.flag === 'likely_not_genuine' ? 'high' : 'check'}"
|
||||||
|
title="${esc(auth.flag === 'likely_not_genuine' ? 'Possibly not a genuine card' : 'Worth double-checking')}"></span>`
|
||||||
|
: '';
|
||||||
|
return `
|
||||||
<tr data-history-row="${g.id}">
|
<tr data-history-row="${g.id}">
|
||||||
<td>
|
<td>
|
||||||
<div class="cardcell">
|
<div class="cardcell">
|
||||||
${g.thumbnail ? `<img src="${g.thumbnail}" alt="">` : '<span class="thumb-blank"></span>'}
|
${g.thumbnail ? `<img src="${g.thumbnail}" alt="">` : '<span class="thumb-blank"></span>'}
|
||||||
<div>
|
<div>
|
||||||
<div class="cardcell-name">${esc(g.label || g.card_note || 'Untitled card')}</div>
|
<div class="cardcell-name">${authDot}${esc(g.label || g.card_note || 'Untitled card')}</div>
|
||||||
<div class="cardcell-meta">${g.card_type ? `${esc(TYPE_LABEL[g.card_type] || 'Card')} · ` : ''}${g.image_count || 1} photo(s)</div>
|
<div class="cardcell-meta">${g.card_type ? `${esc(TYPE_LABEL[g.card_type] || 'Card')} · ` : ''}${g.image_count || 1} photo(s)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -420,7 +437,8 @@ function renderHistory() {
|
||||||
<button class="btn btn-quiet btn-sm" data-history-delete="${g.id}">Delete</button>
|
<button class="btn btn-quiet btn-sm" data-history-delete="${g.id}">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>`).join('');
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------- spend */
|
/* ----------------------------------------------------------------- spend */
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,43 @@ textarea { resize: vertical; min-height: 64px; }
|
||||||
margin-top: 0; margin-bottom: 14px;
|
margin-top: 0; margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------- authenticity */
|
||||||
|
/* A concern about whether the card is what it claims to be, not its
|
||||||
|
condition — deliberately styled as a full banner rather than a hint line
|
||||||
|
like every other measurement caveat, so it reads as "stop and look at
|
||||||
|
this" before anything about the grade itself. Two tiers, both using
|
||||||
|
--critical rather than --brand: this is exactly the "distrust this"
|
||||||
|
signal that colour is reserved for elsewhere in the app, and the brand
|
||||||
|
red would blur into decoration here of all places. */
|
||||||
|
.authenticity-banner {
|
||||||
|
border: 1px solid color-mix(in srgb, var(--critical) 45%, transparent);
|
||||||
|
background: color-mix(in srgb, var(--critical) 10%, var(--surface-1));
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 11px 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.authenticity-check {
|
||||||
|
border-color: color-mix(in srgb, var(--warning) 55%, transparent);
|
||||||
|
background: color-mix(in srgb, var(--warning) 12%, var(--surface-1));
|
||||||
|
}
|
||||||
|
.authenticity-title {
|
||||||
|
font-weight: 700; font-size: 13.5px;
|
||||||
|
color: var(--critical);
|
||||||
|
}
|
||||||
|
.authenticity-check .authenticity-title { color: var(--warning); }
|
||||||
|
.authenticity-detail {
|
||||||
|
font-size: 12.5px; color: var(--ink-2); margin-top: 3px; line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* History-row version: a plain dot, since the row has no room for the full
|
||||||
|
banner text and opening the card gives the real explanation. */
|
||||||
|
.authenticity-dot {
|
||||||
|
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
|
||||||
|
margin-right: 6px; vertical-align: middle;
|
||||||
|
background: var(--critical);
|
||||||
|
}
|
||||||
|
.authenticity-dot.authenticity-check { background: var(--warning); }
|
||||||
|
|
||||||
/* -------------------------------------------------------------- banner */
|
/* -------------------------------------------------------------- banner */
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
|
|
|
||||||
18
store.py
18
store.py
|
|
@ -37,6 +37,7 @@ CREATE TABLE IF NOT EXISTS grades (
|
||||||
grade_high INTEGER,
|
grade_high INTEGER,
|
||||||
confidence TEXT,
|
confidence TEXT,
|
||||||
categories_json TEXT,
|
categories_json TEXT,
|
||||||
|
authenticity_json TEXT,
|
||||||
edge_measurements_json TEXT,
|
edge_measurements_json TEXT,
|
||||||
centering_measurement_json TEXT,
|
centering_measurement_json TEXT,
|
||||||
aspect_measurement_json TEXT,
|
aspect_measurement_json TEXT,
|
||||||
|
|
@ -126,6 +127,7 @@ def init():
|
||||||
for statement in (
|
for statement in (
|
||||||
"ALTER TABLE grades ADD COLUMN aspect_measurement_json TEXT",
|
"ALTER TABLE grades ADD COLUMN aspect_measurement_json TEXT",
|
||||||
"ALTER TABLE grades ADD COLUMN source_images_json TEXT",
|
"ALTER TABLE grades ADD COLUMN source_images_json TEXT",
|
||||||
|
"ALTER TABLE grades ADD COLUMN authenticity_json TEXT",
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
conn.execute(statement)
|
conn.execute(statement)
|
||||||
|
|
@ -219,11 +221,12 @@ def save_grade(grade, thumbnail=None, label=None, source_images=None):
|
||||||
"(created_at, label, card_type, card_note, image_count, thumbnail, "
|
"(created_at, label, card_type, card_note, image_count, thumbnail, "
|
||||||
" model, estimated_grade, "
|
" model, estimated_grade, "
|
||||||
" grade_low, grade_high, confidence, categories_json, "
|
" grade_low, grade_high, confidence, categories_json, "
|
||||||
|
" authenticity_json, "
|
||||||
" edge_measurements_json, centering_measurement_json, "
|
" edge_measurements_json, centering_measurement_json, "
|
||||||
" aspect_measurement_json, "
|
" aspect_measurement_json, "
|
||||||
" limitations_json, note, estimated_cost, usage_json, "
|
" limitations_json, note, estimated_cost, usage_json, "
|
||||||
" source_images_json) "
|
" source_images_json) "
|
||||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
(
|
(
|
||||||
now(), label, grade.get("card_type"), grade.get("card_note"),
|
now(), label, grade.get("card_type"), grade.get("card_note"),
|
||||||
grade.get("image_count"), thumbnail,
|
grade.get("image_count"), thumbnail,
|
||||||
|
|
@ -231,6 +234,7 @@ def save_grade(grade, thumbnail=None, label=None, source_images=None):
|
||||||
grade.get("estimated_grade"), grade.get("grade_low"),
|
grade.get("estimated_grade"), grade.get("grade_low"),
|
||||||
grade.get("grade_high"), grade.get("confidence"),
|
grade.get("grade_high"), grade.get("confidence"),
|
||||||
json.dumps(grade.get("categories")),
|
json.dumps(grade.get("categories")),
|
||||||
|
json.dumps(grade.get("authenticity")),
|
||||||
json.dumps(grade.get("edge_measurements")),
|
json.dumps(grade.get("edge_measurements")),
|
||||||
json.dumps(grade.get("centering_measurement")),
|
json.dumps(grade.get("centering_measurement")),
|
||||||
json.dumps(grade.get("aspect_measurement")),
|
json.dumps(grade.get("aspect_measurement")),
|
||||||
|
|
@ -264,6 +268,7 @@ def update_grade_result(grade_id, grade, thumbnail=None, source_images=None):
|
||||||
grade.get("estimated_grade"), grade.get("grade_low"),
|
grade.get("estimated_grade"), grade.get("grade_low"),
|
||||||
grade.get("grade_high"), grade.get("confidence"),
|
grade.get("grade_high"), grade.get("confidence"),
|
||||||
json.dumps(grade.get("categories")),
|
json.dumps(grade.get("categories")),
|
||||||
|
json.dumps(grade.get("authenticity")),
|
||||||
json.dumps(grade.get("edge_measurements")),
|
json.dumps(grade.get("edge_measurements")),
|
||||||
json.dumps(grade.get("centering_measurement")),
|
json.dumps(grade.get("centering_measurement")),
|
||||||
json.dumps(grade.get("aspect_measurement")),
|
json.dumps(grade.get("aspect_measurement")),
|
||||||
|
|
@ -274,7 +279,7 @@ def update_grade_result(grade_id, grade, thumbnail=None, source_images=None):
|
||||||
sql = (
|
sql = (
|
||||||
"UPDATE grades SET created_at=?, card_type=?, card_note=?, "
|
"UPDATE grades SET created_at=?, card_type=?, card_note=?, "
|
||||||
"image_count=?, model=?, estimated_grade=?, grade_low=?, "
|
"image_count=?, model=?, estimated_grade=?, grade_low=?, "
|
||||||
"grade_high=?, confidence=?, categories_json=?, "
|
"grade_high=?, confidence=?, categories_json=?, authenticity_json=?, "
|
||||||
"edge_measurements_json=?, centering_measurement_json=?, "
|
"edge_measurements_json=?, centering_measurement_json=?, "
|
||||||
"aspect_measurement_json=?, limitations_json=?, note=?, "
|
"aspect_measurement_json=?, limitations_json=?, note=?, "
|
||||||
"estimated_cost=?, usage_json=?"
|
"estimated_cost=?, usage_json=?"
|
||||||
|
|
@ -404,7 +409,7 @@ def get_grade_images(grade_id):
|
||||||
|
|
||||||
def _row_to_grade(row):
|
def _row_to_grade(row):
|
||||||
d = dict(row)
|
d = dict(row)
|
||||||
for key in ("categories_json", "edge_measurements_json",
|
for key in ("categories_json", "authenticity_json", "edge_measurements_json",
|
||||||
"centering_measurement_json", "aspect_measurement_json",
|
"centering_measurement_json", "aspect_measurement_json",
|
||||||
"limitations_json", "usage_json"):
|
"limitations_json", "usage_json"):
|
||||||
out_key = key[:-len("_json")]
|
out_key = key[:-len("_json")]
|
||||||
|
|
@ -429,9 +434,10 @@ def _row_to_grade(row):
|
||||||
_LIST_COLUMNS = (
|
_LIST_COLUMNS = (
|
||||||
"id, created_at, label, card_type, card_note, image_count, thumbnail, "
|
"id, created_at, label, card_type, card_note, image_count, thumbnail, "
|
||||||
"model, estimated_grade, grade_low, grade_high, confidence, "
|
"model, estimated_grade, grade_low, grade_high, confidence, "
|
||||||
"categories_json, edge_measurements_json, centering_measurement_json, "
|
"categories_json, authenticity_json, edge_measurements_json, "
|
||||||
"aspect_measurement_json, limitations_json, note, estimated_cost, "
|
"centering_measurement_json, aspect_measurement_json, limitations_json, "
|
||||||
"usage_json, (source_images_json IS NOT NULL) AS source_images_json"
|
"note, estimated_cost, usage_json, "
|
||||||
|
"(source_images_json IS NOT NULL) AS source_images_json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
80
vision.py
80
vision.py
|
|
@ -521,6 +521,51 @@ the untouched crop too.
|
||||||
On chrome and refractor stock, expect fine scratching; it is the norm rather \
|
On chrome and refractor stock, expect fine scratching; it is the norm rather \
|
||||||
than the exception, and its absence is what is notable.
|
than the exception, and its absence is what is notable.
|
||||||
|
|
||||||
|
AUTHENTICITY — separate from all four categories above, and separate from \
|
||||||
|
condition entirely. A pristine card that isn't a genuine product doesn't get \
|
||||||
|
a PSA grade at all; PSA authenticates before it grades, and a card that fails \
|
||||||
|
that step never reaches the 1-10 scale regardless of how clean it looks. \
|
||||||
|
Judge this from what's actually visible, the same discipline as everything \
|
||||||
|
else here — flag it when you see a real, specific tell, not as a hedge.
|
||||||
|
Signs the card may not be an official print at all: a set, era, or \
|
||||||
|
name/number combination that doesn't correspond to any real printing you \
|
||||||
|
recognize; card text in a font, layout, or language inconsistent with the \
|
||||||
|
publisher and era it's claiming to be; artwork that reads as fan-made or \
|
||||||
|
AI-generated rather than the actual official illustration for that card; \
|
||||||
|
visible printing quality far below what mass-produced card stock looks like \
|
||||||
|
— soft/pixelated detail, visible inkjet or laser-printer banding, a paper \
|
||||||
|
stock that looks like cardstock from a home printer rather than the glossy, \
|
||||||
|
consistent stock real trading cards use; or the word "proxy," "custom," \
|
||||||
|
"fan art," or similar printed anywhere on the card itself.
|
||||||
|
Signs a real card might be a counterfeit of a genuine printing: colour \
|
||||||
|
saturation, font weight, or line quality that doesn't match known genuine \
|
||||||
|
copies of that exact card; a holo or foil pattern that looks wrong for the \
|
||||||
|
set (wrong pattern entirely, or applied where the real printing doesn't have \
|
||||||
|
one); a cardback design, layout, or copyright text that doesn't match the \
|
||||||
|
genuine card. Weigh these against your own uncertainty — you're comparing \
|
||||||
|
against a memory of what genuine copies look like, not a reference image, so \
|
||||||
|
reserve likely_not_genuine for cases where something is clearly, specifically \
|
||||||
|
wrong, not just "this looks a little different than I'd expect."
|
||||||
|
TRIMMING is the physical-alteration version of this same concern, already \
|
||||||
|
covered under CORNERS above — a genuine card that's been cut down to fake \
|
||||||
|
sharper edges than it actually has. When you flag trimming there, also set \
|
||||||
|
this field (worth_checking is usually right, since a photo can't confirm it \
|
||||||
|
the way calipers can) so it surfaces in one place rather than only inside a \
|
||||||
|
corners observation.
|
||||||
|
flag "none" is the ordinary case and should be the overwhelming majority \
|
||||||
|
of cards — most cards people photograph for grading are exactly what they \
|
||||||
|
appear to be, and reaching for suspicion without a specific tell is its own \
|
||||||
|
kind of dishonesty, the same failure as inventing wear that isn't there. Use \
|
||||||
|
"worth_checking" when you see something specific but can't be confident from \
|
||||||
|
a photo alone — say what you saw. Use "likely_not_genuine" only when you're \
|
||||||
|
reasonably confident, not just uneasy — a fictional set name, visible \
|
||||||
|
"proxy" text, or artwork that plainly isn't the real official print are the \
|
||||||
|
kind of thing that earns this level; a card that's merely unfamiliar to you \
|
||||||
|
does not.
|
||||||
|
If you flag likely_not_genuine, say so in note and reflect it in the grade: \
|
||||||
|
estimated_grade should be null (this isn't a card PSA would assign a number \
|
||||||
|
to at all), with the reasoning in note rather than in a numeric range.
|
||||||
|
|
||||||
HOW PSA COMBINES THE FOUR
|
HOW PSA COMBINES THE FOUR
|
||||||
|
|
||||||
The grade is capped by the WORST attribute, not averaged across them. Three \
|
The grade is capped by the WORST attribute, not averaged across them. Three \
|
||||||
|
|
@ -670,6 +715,25 @@ GRADE_CATEGORY_SCHEMA = {
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AUTHENTICITY_SCHEMA = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"flag": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["none", "worth_checking", "likely_not_genuine"],
|
||||||
|
"description": "Whether anything about this card's legitimacy, "
|
||||||
|
"not its condition, is in question.",
|
||||||
|
},
|
||||||
|
"observation": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The specific thing that prompted the flag — "
|
||||||
|
"empty string when flag is 'none'.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["flag", "observation"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
}
|
||||||
|
|
||||||
GRADING_SCHEMA = {
|
GRADING_SCHEMA = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -682,6 +746,7 @@ GRADING_SCHEMA = {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "One line naming the card if legible (player/name, set, year) — for the history list.",
|
"description": "One line naming the card if legible (player/name, set, year) — for the history list.",
|
||||||
},
|
},
|
||||||
|
"authenticity": AUTHENTICITY_SCHEMA,
|
||||||
"estimated_grade": {
|
"estimated_grade": {
|
||||||
"type": ["integer", "null"],
|
"type": ["integer", "null"],
|
||||||
"description": "Best single estimate, whole number 1-10, or null if ungradeable from these photos.",
|
"description": "Best single estimate, whole number 1-10, or null if ungradeable from these photos.",
|
||||||
|
|
@ -699,9 +764,9 @@ GRADING_SCHEMA = {
|
||||||
},
|
},
|
||||||
"note": {"type": "string", "description": "Short overall summary."},
|
"note": {"type": "string", "description": "Short overall summary."},
|
||||||
},
|
},
|
||||||
"required": ["card_type", "card_note", "estimated_grade", "grade_low",
|
"required": ["card_type", "card_note", "authenticity", "estimated_grade",
|
||||||
"grade_high", "confidence", "centering", "corners", "edges",
|
"grade_low", "grade_high", "confidence", "centering", "corners",
|
||||||
"surface", "limitations", "note"],
|
"edges", "surface", "limitations", "note"],
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -720,6 +785,14 @@ def _clean_category(raw):
|
||||||
return {"severity": severity, "observation": raw.get("observation") or ""}
|
return {"severity": severity, "observation": raw.get("observation") or ""}
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_authenticity(raw):
|
||||||
|
raw = raw if isinstance(raw, dict) else {}
|
||||||
|
flag = raw.get("flag")
|
||||||
|
if flag not in ("none", "worth_checking", "likely_not_genuine"):
|
||||||
|
flag = "none"
|
||||||
|
return {"flag": flag, "observation": raw.get("observation") or ""}
|
||||||
|
|
||||||
|
|
||||||
def grade_card(images, api_key=None, model=None, effort=None, zoom_details=True):
|
def grade_card(images, api_key=None, model=None, effort=None, zoom_details=True):
|
||||||
"""Estimate the PSA grade a card would likely receive, from photo(s).
|
"""Estimate the PSA grade a card would likely receive, from photo(s).
|
||||||
|
|
||||||
|
|
@ -924,6 +997,7 @@ def grade_card(images, api_key=None, model=None, effort=None, zoom_details=True)
|
||||||
"closeups": len(crops),
|
"closeups": len(crops),
|
||||||
"card_type": card_type,
|
"card_type": card_type,
|
||||||
"card_note": (parsed.get("card_note") or "").strip(),
|
"card_note": (parsed.get("card_note") or "").strip(),
|
||||||
|
"authenticity": _clean_authenticity(parsed.get("authenticity")),
|
||||||
"edge_measurements": measured,
|
"edge_measurements": measured,
|
||||||
"centering_measurement": centering,
|
"centering_measurement": centering,
|
||||||
"aspect_measurement": aspect,
|
"aspect_measurement": aspect,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue