This commit is contained in:
2026-08-17 01:30:28 +02:00
parent c2429873b3
commit 0dceb3a404
18 changed files with 1021 additions and 219 deletions
+139 -47
View File
@@ -358,6 +358,9 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
}
weakDesc := descriptionNeedsEnhanceRepair(desc, descTpl, name)
enhanceMetaTitle, enhanceMetaDesc := enhanceMetaFromRaw(raw)
if in.OmitSEOMeta {
enhanceMetaTitle, enhanceMetaDesc = "", ""
}
// Only persist enhance_input_hash for quality ok / hash-skip unchanged.
// Never copy input_hash from error/passthrough/thin/synthesized meta.
persistHash := ""
@@ -1391,7 +1394,7 @@ func preferredProductTitle(gtin string, candidates ...string) string {
if c == "" || c == "<nil>" || isPromptLabelTitle(c) {
continue
}
usable = append(usable, c)
usable = append(usable, ensureReadableTitleSpacing(c))
}
for _, c := range usable {
if isBrandOnlyTitleAmong(c, usable) {
@@ -1479,6 +1482,9 @@ func preferredProductDescription(title string, candidates ...string) string {
if containsWeakFillerPhrase(c) {
continue
}
if company.LooksLikeHeuristicSynthesize(c) {
continue
}
return SanitizeOutput(c)
}
return ""
@@ -1508,15 +1514,20 @@ func attrLookupCI(attrs map[string]any, keys ...string) string {
}
func formatAttrDimParts(attrs map[string]any, maxParts int) []string {
return formatAttrDimPartsLang(attrs, maxParts, "")
}
func formatAttrDimPartsLang(attrs map[string]any, maxParts int, language string) []string {
if attrs == nil || maxParts <= 0 {
return nil
}
prefer := []string{
"width", "height", "depth", "weight", "max_load", "max load", "load_capacity",
"vesa", "screen_size", "diagonal", "color", "material", "size",
"vesa", "screen_size", "diagonal", "color", "material", "size", "energy_class", "warranty",
}
parts := make([]string, 0, maxParts)
seen := map[string]struct{}{}
sl := isSlovenianContentLanguage(language)
add := func(k, v string) {
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
@@ -1527,8 +1538,11 @@ func formatAttrDimParts(attrs map[string]any, maxParts int) []string {
if _, ok := seen[lk]; ok {
return
}
if isDimensionKey(canonicalizeAttrKey(k)) && isZeroishString(v) {
return
}
seen[lk] = struct{}{}
parts = append(parts, fmt.Sprintf("%s %s", k, v))
parts = append(parts, fmt.Sprintf("%s: %s", attrDimLabel(k, sl), v))
}
for _, k := range prefer {
if len(parts) >= maxParts {
@@ -1541,9 +1555,61 @@ func formatAttrDimParts(attrs map[string]any, maxParts int) []string {
return parts
}
func attrDimLabel(key string, slovenian bool) string {
canon := canonicalizeAttrKey(key)
if slovenian {
switch canon {
case "width":
return "Širina"
case "height":
return "Višina"
case "depth":
return "Globina"
case "weight":
return "Teža"
case "energy_class":
return "Energijski razred"
case "warranty":
return "Garancija"
case "color":
return "Barva"
case "material":
return "Material"
case "size", "screen_size", "diagonal":
return "Velikost"
}
}
switch canon {
case "width":
return "Width"
case "height":
return "Height"
case "depth":
return "Depth"
case "weight":
return "Weight"
case "energy_class":
return "Energy class"
case "warranty":
return "Warranty"
case "max_load", "load_capacity":
return "Max load"
case "screen_size", "diagonal":
return "Screen size"
case "product_model":
return "Model"
default:
if canon == "" {
return key
}
return strings.ReplaceAll(canon, "_", " ")
}
}
// synthesizeProductDescription prefers a category description_template skeleton
// (HTML section types) when present; otherwise falls back to plain title synthesize.
func synthesizeProductDescription(title, category, language string, attrs map[string]any, descriptionTemplate any) string {
title = ensureReadableTitleSpacing(title)
if sections, ok := parseDescriptionFormulaSections(descriptionTemplate); ok && len(sections) > 0 {
if out := synthesizeDescriptionFromFormula(title, category, language, attrs, sections); out != "" {
return out
@@ -1555,23 +1621,30 @@ func synthesizeProductDescription(title, category, language string, attrs map[st
// synthesizeDescriptionFromFormula builds a minimal HTML description matching
// category description_template section types so timeout/fallback still respects
// A1 category structure (unlike plain title synthesize).
// Duplicate paragraph/list section types are emitted once to avoid invent-boilerplate
// repetition (legacy bug: every <p> repeated the same synthesize sentence).
func synthesizeDescriptionFromFormula(title, category, language string, attrs map[string]any, sections []descriptionFormulaSection) string {
title = strings.TrimSpace(title)
title = ensureReadableTitleSpacing(strings.TrimSpace(title))
if title == "" || title == "<nil>" || isPromptLabelTitle(title) {
return ""
}
base := synthesizeDescriptionFromTitle(title, category, language, attrs)
if base == "" {
dims := formatAttrDimPartsLang(attrs, 6, language)
intro := factualDescriptionIntro(title, category, language, attrs)
if intro == "" {
return ""
}
dims := formatAttrDimParts(attrs, 6)
var b strings.Builder
paraUsed := false
listUsed := false
headingLevels := map[string]bool{}
for _, s := range sections {
typ := strings.ToLower(strings.TrimSpace(s.Type))
switch typ {
case "h1", "h2", "h3", "h4":
if headingLevels[typ] {
continue
}
headingLevels[typ] = true
heading := title
if typ != "h1" {
if isSlovenianContentLanguage(language) {
@@ -1582,37 +1655,42 @@ func synthesizeDescriptionFromFormula(title, category, language string, attrs ma
}
fmt.Fprintf(&b, "<%s>%s</%s>", typ, SanitizeOutput(heading), typ)
case "ul":
b.WriteString("<ul>")
if listUsed {
continue
}
items := dims
if len(items) == 0 {
items = []string{base}
if secondary := factualSecondaryFacts(language, attrs); len(secondary) > 0 {
items = secondary
} else {
items = []string{intro}
}
}
b.WriteString("<ul>")
for _, it := range items {
fmt.Fprintf(&b, "<li>%s</li>", SanitizeOutput(it))
}
b.WriteString("</ul>")
listUsed = true
default: // p and unknown → paragraph
body := base
if paraUsed && len(dims) > 0 && !listUsed {
if isSlovenianContentLanguage(language) {
body = "Ključne specifikacije: " + strings.Join(dims, ", ") + "."
} else {
body = "Key specs: " + strings.Join(dims, ", ") + "."
}
default: // p and unknown → paragraph (once)
if paraUsed {
continue
}
fmt.Fprintf(&b, "<p>%s</p>", SanitizeOutput(body))
fmt.Fprintf(&b, "<p>%s</p>", SanitizeOutput(intro))
paraUsed = true
}
}
return SanitizeOutput(b.String())
out := strings.TrimSpace(b.String())
if out == "" {
return ""
}
return SanitizeOutput(out)
}
// synthesizeDescriptionFromTitle builds a short factual fallback when enhance
// returns empty/title-echo/filler copy. Uses title, category, brand, model, and
// key dims. language is a content-language code (en/sl/…) or English label.
func synthesizeDescriptionFromTitle(title, category, language string, attrs map[string]any) string {
title = strings.TrimSpace(title)
// factualDescriptionIntro builds a short non-invent paragraph for formula fallback.
// Intentionally avoids heuristicSynthesizePhrases ("je izdelek v kategoriji", …).
func factualDescriptionIntro(title, category, language string, attrs map[string]any) string {
title = ensureReadableTitleSpacing(strings.TrimSpace(title))
if title == "" || title == "<nil>" || isPromptLabelTitle(title) {
return ""
}
@@ -1622,59 +1700,73 @@ func synthesizeDescriptionFromTitle(title, category, language string, attrs map[
}
brand := attrLookupCI(attrs, "brand")
model := attrLookupCI(attrs, "product_model", "model", "sku")
dims := formatAttrDimParts(attrs, 3)
dims := formatAttrDimPartsLang(attrs, 3, language)
sl := isSlovenianContentLanguage(language)
var b strings.Builder
b.WriteString(title)
if sl {
b.WriteString(title)
switch {
case cat != "" && brand != "":
fmt.Fprintf(&b, " je izdelek v kategoriji %s znamke %s", cat, brand)
case cat != "":
fmt.Fprintf(&b, " je izdelek v kategoriji %s", cat)
case brand != "" && cat != "":
fmt.Fprintf(&b, " %s, znamka %s", cat, brand)
case brand != "":
fmt.Fprintf(&b, " je izdelek znamke %s", brand)
fmt.Fprintf(&b, " znamka %s", brand)
case cat != "":
fmt.Fprintf(&b, " — %s", cat)
default:
b.WriteString(" je katalogski izdelek z znanimi atributi")
b.WriteString(" katalogski izdelek")
}
if model != "" && !strings.Contains(strings.ToLower(title), strings.ToLower(model)) {
fmt.Fprintf(&b, " (model %s)", model)
}
if len(dims) > 0 {
fmt.Fprintf(&b, ". Ključne specifikacije: %s", strings.Join(dims, ", "))
fmt.Fprintf(&b, ". %s", strings.Join(dims, ", "))
}
b.WriteByte('.')
} else {
b.WriteString(title)
switch {
case cat != "" && brand != "":
fmt.Fprintf(&b, " is a %s product from %s", cat, brand)
case cat != "":
fmt.Fprintf(&b, " is listed in the %s category", cat)
case brand != "" && cat != "":
fmt.Fprintf(&b, " — %s from %s", cat, brand)
case brand != "":
fmt.Fprintf(&b, " is a product from %s", brand)
fmt.Fprintf(&b, " from %s", brand)
case cat != "":
fmt.Fprintf(&b, " — %s", cat)
default:
b.WriteString(" is a catalog product with the known attributes")
b.WriteString(" catalog product")
}
if model != "" && !strings.Contains(strings.ToLower(title), strings.ToLower(model)) {
fmt.Fprintf(&b, " (model %s)", model)
}
if len(dims) > 0 {
fmt.Fprintf(&b, ". Key specs: %s", strings.Join(dims, ", "))
fmt.Fprintf(&b, ". %s", strings.Join(dims, ", "))
}
b.WriteByte('.')
}
out := SanitizeOutput(b.String())
// Never emit sole retail-filler when title/attrs exist — strip legacy phrase if any helper reintroduces it.
if containsWeakFillerPhrase(out) {
out = strings.TrimSpace(strings.ReplaceAll(out, "Ready for retail listing.", ""))
out = strings.TrimSpace(strings.ReplaceAll(out, "ready for retail listing.", ""))
out = strings.TrimSpace(strings.Trim(out, ".")) + "."
return SanitizeOutput(b.String())
}
func factualSecondaryFacts(language string, attrs map[string]any) []string {
prefer := []string{"energy_class", "warranty", "color", "material"}
sl := isSlovenianContentLanguage(language)
var out []string
for _, k := range prefer {
v := attrLookupCI(attrs, k)
if v == "" || isZeroishString(v) {
continue
}
out = append(out, fmt.Sprintf("%s: %s", attrDimLabel(k, sl), v))
}
return out
}
// synthesizeDescriptionFromTitle builds a short factual fallback when enhance
// returns empty/title-echo/filler copy. Uses title, category, brand, model, and
// key dims. language is a content-language code (en/sl/…) or English label.
// Prefer synthesizeProductDescription when a description_template is available.
func synthesizeDescriptionFromTitle(title, category, language string, attrs map[string]any) string {
return factualDescriptionIntro(title, category, language, attrs)
}
func isSlovenianContentLanguage(raw string) bool {
raw = strings.TrimSpace(raw)
if raw == "" {