fix
This commit is contained in:
@@ -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 == "" {
|
||||
|
||||
Reference in New Issue
Block a user