Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
A
ai company
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
ai company
Commits
20098dc6
Commit
20098dc6
authored
May 21, 2026
by
Vũ Hoàng Anh
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add di_hoc occasion mapping and reason text sanitization for kid products
parent
0e3f8b6a
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
77 additions
and
5 deletions
+77
-5
index.html
backend/static/manage-ultra-desc/index.html
+24
-2
fashion_rules.json
backend/worker/fashion_rules.json
+11
-0
stylist_engine.py
backend/worker/stylist_engine.py
+42
-3
No files found.
backend/static/manage-ultra-desc/index.html
View file @
20098dc6
...
@@ -459,6 +459,7 @@
...
@@ -459,6 +459,7 @@
}
}
.occ-badge-hang_ngay
{
background
:
#eef1f8
;
color
:
#3b5998
;
}
.occ-badge-hang_ngay
{
background
:
#eef1f8
;
color
:
#3b5998
;
}
.occ-badge-di_lam
{
background
:
#ecfdf5
;
color
:
#16a34a
;
}
.occ-badge-di_lam
{
background
:
#ecfdf5
;
color
:
#16a34a
;
}
.occ-badge-di_hoc
{
background
:
#e0e7ff
;
color
:
#4f46e5
;
}
.occ-badge-di_choi
{
background
:
#fef3c7
;
color
:
#d97706
;
}
.occ-badge-di_choi
{
background
:
#fef3c7
;
color
:
#d97706
;
}
.occ-badge-the_thao
{
background
:
#fce7f3
;
color
:
#db2777
;
}
.occ-badge-the_thao
{
background
:
#fce7f3
;
color
:
#db2777
;
}
.occ-badge-mac_nha
{
background
:
#f3e8ff
;
color
:
#7c3aed
;
}
.occ-badge-mac_nha
{
background
:
#f3e8ff
;
color
:
#7c3aed
;
}
...
@@ -540,6 +541,14 @@
...
@@ -540,6 +541,14 @@
let
currentSimilarItems
=
[];
let
currentSimilarItems
=
[];
let
currentOutfits
=
[];
let
currentOutfits
=
[];
function
isKidProduct
()
{
if
(
!
currentProduct
)
return
false
;
const
age
=
(
currentProduct
.
age_by_product
||
""
).
toLowerCase
();
const
gender
=
(
currentProduct
.
gender_by_product
||
""
).
toLowerCase
();
const
name
=
(
currentProduct
.
product_name
||
""
).
toLowerCase
();
return
age
===
'kid'
||
age
===
'baby'
||
gender
===
'be_trai'
||
gender
===
'be_gai'
||
name
.
includes
(
"trẻ em"
)
||
name
.
includes
(
"bé trai"
)
||
name
.
includes
(
"bé gái"
)
||
name
.
includes
(
"kid"
);
}
// ═══ IMAGE URL FORMATTER ═══
// ═══ IMAGE URL FORMATTER ═══
function
formatImageUrl
(
url
)
{
function
formatImageUrl
(
url
)
{
if
(
!
url
)
return
''
;
if
(
!
url
)
return
''
;
...
@@ -1101,7 +1110,12 @@
...
@@ -1101,7 +1110,12 @@
currentProduct
=
data
.
product
;
currentProduct
=
data
.
product
;
window
.
currentProduct
=
currentProduct
;
window
.
currentProduct
=
currentProduct
;
currentSimilarItems
=
currentProduct
.
parsed_similar_items
||
[];
currentSimilarItems
=
currentProduct
.
parsed_similar_items
||
[];
currentOutfits
=
currentProduct
.
parsed_outfit_recommendations
||
[];
currentOutfits
=
(
currentProduct
.
parsed_outfit_recommendations
||
[]).
map
(
o
=>
{
if
(
o
.
occasion
===
'di_lam'
&&
isKidProduct
())
{
o
.
occasion
=
'di_hoc'
;
}
return
o
;
});
// Update sidebar state badges to match current database state
// Update sidebar state badges to match current database state
const
hasSim
=
currentSimilarItems
.
length
>
0
;
const
hasSim
=
currentSimilarItems
.
length
>
0
;
...
@@ -1158,7 +1172,12 @@
...
@@ -1158,7 +1172,12 @@
currentProduct
=
data
.
product
;
currentProduct
=
data
.
product
;
window
.
currentProduct
=
currentProduct
;
window
.
currentProduct
=
currentProduct
;
currentSimilarItems
=
currentProduct
.
parsed_similar_items
||
[];
currentSimilarItems
=
currentProduct
.
parsed_similar_items
||
[];
currentOutfits
=
currentProduct
.
parsed_outfit_recommendations
||
[];
currentOutfits
=
(
currentProduct
.
parsed_outfit_recommendations
||
[]).
map
(
o
=>
{
if
(
o
.
occasion
===
'di_lam'
&&
isKidProduct
())
{
o
.
occasion
=
'di_hoc'
;
}
return
o
;
});
// Cập nhật trạng thái sidebar dựa trên dữ liệu mới của biến thể màu
// Cập nhật trạng thái sidebar dựa trên dữ liệu mới của biến thể màu
const
baseCode
=
currentProduct
.
magento_ref_code
||
currentProduct
.
internal_ref_code
;
const
baseCode
=
currentProduct
.
magento_ref_code
||
currentProduct
.
internal_ref_code
;
...
@@ -1493,6 +1512,7 @@
...
@@ -1493,6 +1512,7 @@
const
OCC_LABELS
=
{
const
OCC_LABELS
=
{
'hang_ngay'
:
'Hàng ngày'
,
'hang_ngay'
:
'Hàng ngày'
,
'di_lam'
:
'Đi làm'
,
'di_lam'
:
'Đi làm'
,
'di_hoc'
:
'Đi học'
,
'di_choi'
:
'Đi chơi'
,
'di_choi'
:
'Đi chơi'
,
'di_tiec'
:
'Đi tiệc'
,
'di_tiec'
:
'Đi tiệc'
,
'the_thao'
:
'Thể thao'
,
'the_thao'
:
'Thể thao'
,
...
@@ -1624,6 +1644,7 @@
...
@@ -1624,6 +1644,7 @@
color_name
:
p
.
product_color_name
||
''
,
color_name
:
p
.
product_color_name
||
''
,
match_product_name
:
p
.
product_name
,
match_product_name
:
p
.
product_name
,
role
:
"bottom"
,
role
:
"bottom"
,
occasion
:
activeOccFilter
||
(
isKidProduct
()
?
"di_hoc"
:
"hang_ngay"
),
reason
:
`Phối cùng
${
p
.
product_name
}
giúp tôn lên phong cách trẻ trung và thoải mái khi diện hàng ngày.`
,
reason
:
`Phối cùng
${
p
.
product_name
}
giúp tôn lên phong cách trẻ trung và thoải mái khi diện hàng ngày.`
,
product_image_url_thumbnail
:
p
.
product_image_url_thumbnail
product_image_url_thumbnail
:
p
.
product_image_url_thumbnail
});
});
...
@@ -1738,6 +1759,7 @@
...
@@ -1738,6 +1759,7 @@
color_name
:
item
.
color_name
||
''
,
color_name
:
item
.
color_name
||
''
,
match_product_name
:
item
.
match_product_name
,
match_product_name
:
item
.
match_product_name
,
role
:
item
.
role
||
'bottom'
,
role
:
item
.
role
||
'bottom'
,
occasion
:
item
.
occasion
||
(
isKidProduct
()
?
'di_hoc'
:
'hang_ngay'
),
reason
:
item
.
reason
||
''
reason
:
item
.
reason
||
''
}));
}));
...
...
backend/worker/fashion_rules.json
View file @
20098dc6
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
"occasions"
:
[
"occasions"
:
[
"hang_ngay"
,
"hang_ngay"
,
"di_lam"
,
"di_lam"
,
"di_hoc"
,
"di_choi"
,
"di_choi"
,
"di_tiec"
,
"di_tiec"
,
"the_thao"
,
"the_thao"
,
...
@@ -11,6 +12,7 @@
...
@@ -11,6 +12,7 @@
"occasion_labels"
:
{
"occasion_labels"
:
{
"hang_ngay"
:
"Hàng ngày"
,
"hang_ngay"
:
"Hàng ngày"
,
"di_lam"
:
"Đi làm công sở"
,
"di_lam"
:
"Đi làm công sở"
,
"di_hoc"
:
"Đi học"
,
"di_choi"
:
"Đi chơi / dạo phố"
,
"di_choi"
:
"Đi chơi / dạo phố"
,
"di_tiec"
:
"Đi tiệc"
,
"di_tiec"
:
"Đi tiệc"
,
"the_thao"
:
"Thể thao"
,
"the_thao"
:
"Thể thao"
,
...
@@ -31,6 +33,15 @@
...
@@ -31,6 +33,15 @@
"work"
,
"work"
,
"office"
"office"
],
],
"di_hoc"
:
[
"đi học"
,
"trường học"
,
"lớp học"
,
"school"
,
"class"
,
"học tập"
,
"vui học"
],
"di_choi"
:
[
"di_choi"
:
[
"đi chơi"
,
"đi chơi"
,
"dạo phố"
,
"dạo phố"
,
...
...
backend/worker/stylist_engine.py
View file @
20098dc6
...
@@ -120,11 +120,19 @@ class StylistEngine:
...
@@ -120,11 +120,19 @@ class StylistEngine:
anchor_cat
=
source
.
get
(
"product_line"
,
""
)
anchor_cat
=
source
.
get
(
"product_line"
,
""
)
gender
=
source
.
get
(
"gender"
,
""
)
gender
=
source
.
get
(
"gender"
,
""
)
is_kid
=
(
source
.
get
(
"age_group"
,
""
)
.
lower
()
==
"kid"
or
source
.
get
(
"gender"
,
""
)
.
lower
()
in
(
"boy"
,
"girl"
,
"be_trai"
,
"be_gai"
)
or
any
(
k
in
source
.
get
(
"name"
,
""
)
.
lower
()
for
k
in
[
"trẻ em"
,
"bé trai"
,
"bé gái"
,
"cho bé"
,
"unisex trẻ em"
])
)
# Fetch allowed mappings from DB
# Fetch allowed mappings from DB
db_rules
=
self
.
_fetch_db_rules
(
anchor_cat
,
gender
)
db_rules
=
self
.
_fetch_db_rules
(
anchor_cat
,
gender
)
allowed_by_occ
=
{}
allowed_by_occ
=
{}
for
r
in
db_rules
:
for
r
in
db_rules
:
occ
=
r
[
"occ"
]
occ
=
r
[
"occ"
]
if
is_kid
and
occ
==
"di_lam"
:
occ
=
"di_hoc"
tgt
=
r
[
"target_cat"
]
.
lower
()
tgt
=
r
[
"target_cat"
]
.
lower
()
if
occ
not
in
allowed_by_occ
:
if
occ
not
in
allowed_by_occ
:
allowed_by_occ
[
occ
]
=
set
()
allowed_by_occ
[
occ
]
=
set
()
...
@@ -159,6 +167,16 @@ class StylistEngine:
...
@@ -159,6 +167,16 @@ class StylistEngine:
if
score
<
rules
.
get
(
"min_score"
,
35
):
if
score
<
rules
.
get
(
"min_score"
,
35
):
continue
continue
reason
=
self
.
_build_reason
(
source
,
target
,
occ
,
score
)
reason
=
self
.
_build_reason
(
source
,
target
,
occ
,
score
)
if
is_kid
:
reason
=
(
reason
.
replace
(
"đi làm công sở"
,
"đi học"
)
.
replace
(
"đi làm"
,
"đi học"
)
.
replace
(
"công sở"
,
"học tập và vui chơi"
)
.
replace
(
"văn phòng"
,
"trường học"
)
.
replace
(
"làm việc"
,
"học tập"
)
.
replace
(
"lịch sự dịp đi làm"
,
"lịch sự dịp đi học"
)
.
replace
(
"phù hợp dịp đi học công sở"
,
"phù hợp dịp đi học"
)
)
buckets
[
occ
][
role
]
.
append
({
buckets
[
occ
][
role
]
.
append
({
"code"
:
target
[
"code"
],
"code"
:
target
[
"code"
],
"name"
:
target
[
"name"
],
"name"
:
target
[
"name"
],
...
@@ -202,6 +220,12 @@ class StylistEngine:
...
@@ -202,6 +220,12 @@ class StylistEngine:
anchor_cat
=
source
.
get
(
"product_line"
,
""
)
anchor_cat
=
source
.
get
(
"product_line"
,
""
)
gender
=
source
.
get
(
"gender"
,
""
)
gender
=
source
.
get
(
"gender"
,
""
)
is_kid
=
(
source
.
get
(
"age_group"
,
""
)
.
lower
()
==
"kid"
or
source
.
get
(
"gender"
,
""
)
.
lower
()
in
(
"boy"
,
"girl"
,
"be_trai"
,
"be_gai"
)
or
any
(
k
in
source
.
get
(
"name"
,
""
)
.
lower
()
for
k
in
[
"trẻ em"
,
"bé trai"
,
"bé gái"
,
"cho bé"
,
"unisex trẻ em"
])
)
# 1. Fetch Rules from DB: list of {occ, role, target_cat, ai_reason}
# 1. Fetch Rules from DB: list of {occ, role, target_cat, ai_reason}
db_rules
=
self
.
_fetch_db_rules
(
anchor_cat
,
gender
)
db_rules
=
self
.
_fetch_db_rules
(
anchor_cat
,
gender
)
if
not
db_rules
:
if
not
db_rules
:
...
@@ -211,9 +235,12 @@ class StylistEngine:
...
@@ -211,9 +235,12 @@ class StylistEngine:
# Group by occasion for easier lookup
# Group by occasion for easier lookup
rules_by_occ
:
dict
[
str
,
list
[
dict
]]
=
{}
rules_by_occ
:
dict
[
str
,
list
[
dict
]]
=
{}
for
r
in
db_rules
:
for
r
in
db_rules
:
if
r
[
"occ"
]
not
in
rules_by_occ
:
occ
=
r
[
"occ"
]
rules_by_occ
[
r
[
"occ"
]]
=
[]
if
is_kid
and
occ
==
"di_lam"
:
rules_by_occ
[
r
[
"occ"
]]
.
append
(
r
)
occ
=
"di_hoc"
if
occ
not
in
rules_by_occ
:
rules_by_occ
[
occ
]
=
[]
rules_by_occ
[
occ
]
.
append
(
r
)
max_items
=
self
.
rules
.
get
(
"role_max_items"
,
{})
max_items
=
self
.
rules
.
get
(
"role_max_items"
,
{})
buckets
:
dict
[
str
,
dict
[
str
,
list
[
dict
]]]
=
{}
buckets
:
dict
[
str
,
dict
[
str
,
list
[
dict
]]]
=
{}
...
@@ -245,6 +272,18 @@ class StylistEngine:
...
@@ -245,6 +272,18 @@ class StylistEngine:
db_reason
=
r
[
"ai_reason"
]
or
f
"Theo định hướng ({occ})"
db_reason
=
r
[
"ai_reason"
]
or
f
"Theo định hướng ({occ})"
combined_reason
=
f
"{algo_reason} | {db_reason}"
combined_reason
=
f
"{algo_reason} | {db_reason}"
if
is_kid
:
# Sanitize reason text to replace work/office terms with school terms
combined_reason
=
(
combined_reason
.
replace
(
"đi làm công sở"
,
"đi học"
)
.
replace
(
"đi làm"
,
"đi học"
)
.
replace
(
"công sở"
,
"học tập và vui chơi"
)
.
replace
(
"văn phòng"
,
"trường học"
)
.
replace
(
"làm việc"
,
"học tập"
)
.
replace
(
"lịch sự dịp đi làm"
,
"lịch sự dịp đi học"
)
.
replace
(
"phù hợp dịp đi học công sở"
,
"phù hợp dịp đi học"
)
)
breakdown
=
self
.
_score_breakdown
(
source
,
target
,
occ
)
breakdown
=
self
.
_score_breakdown
(
source
,
target
,
occ
)
if
occ
not
in
buckets
:
if
occ
not
in
buckets
:
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment