| import json |
| import os |
| from groq import Groq |
|
|
| STYLE_DESCRIPTIONS = { |
| "volume_top": "height on top", |
| "volume_sides": "fuller sides", |
| "short_sides": "tapered sides", |
| "longer_hair": "longer length", |
| "fringe": "front fringe", |
| "clean_lines": "clean shape", |
| "soft_texture": "soft texture", |
| "textured_top": "textured top", |
| "layers": "layered cut", |
| "updo": "lifted style", |
| "curtain_fringe": "curtain fringe", |
| } |
|
|
| STYLE_DESCRIPTIONS_PL = { |
| "volume_top": "objętość na górze", |
| "volume_sides": "pełniejsze boki", |
| "short_sides": "krótkie boki", |
| "longer_hair": "dłuższe włosy", |
| "fringe": "grzywka", |
| "clean_lines": "czysty kształt", |
| "soft_texture": "miękka tekstura", |
| "textured_top": "góra z teksturą", |
| "layers": "warstwowe cięcie", |
| "updo": "upięcie", |
| "curtain_fringe": "kurtynowa grzywka", |
| } |
|
|
| NEGATIVE_EXPLANATIONS = { |
| "fringe": "fringe may not suit your eye proportions or add unwanted weight to the forehead", |
| "volume_sides": "side volume may widen your face shape", |
| "volume_top": "extra height may emphasise the length of your face", |
| "short_sides": "tapered sides may draw attention to a wider jaw", |
| "clean_lines": "sharp geometric cuts can highlight facial asymmetry", |
| "soft_texture": "heavy texture may work against your face's natural structure", |
| "longer_hair": "added length risks elongating your face further", |
| "textured_top": "textured volume on top may unbalance a prominent chin", |
| "layers": "heavy layering may not suit your face proportions", |
| "updo": "lifted styles may elongate your face further", |
| "curtain_fringe": "a centre parting may emphasise close-set eyes", |
| } |
|
|
| NEGATIVE_EXPLANATIONS_PL = { |
| "fringe": "grzywka może zasłaniać oczy lub dodawać wagi czołu", |
| "volume_sides": "objętość boków może optycznie poszerzyć twarz", |
| "volume_top": "dodatkowa wysokość może podkreślić długość twarzy", |
| "short_sides": "krótkie boki mogą zwracać uwagę na szeroką szczękę", |
| "clean_lines": "geometryczne cięcia mogą uwydatniać asymetrię", |
| "soft_texture": "miękka tekstura może nie pasować do struktury twarzy", |
| "longer_hair": "długość może dodatkowo wydłużyć twarz", |
| "textured_top": "teksturowana góra może zaburzyć balans przy wyraźnej brodzie", |
| "layers": "warstwy mogą nie pasować do proporcji twarzy", |
| "updo": "upięcie może wydłużyć twarz", |
| "curtain_fringe": "środkowy przedziałek może uwydatnić blisko osadzone oczy", |
| } |
|
|
| TRAIT_EXPLANATIONS = { |
| "face_length": { |
| "long": "long face shape — styles with side volume and fringe work in your favour", |
| "short": "shorter face shape — height on top helps elongate proportions", |
| "balanced": "face length is well balanced", |
| }, |
| "forehead": { |
| "high": "high forehead — fringe optically lowers the hairline", |
| "low": "low forehead — keep the forehead open, avoid heavy fringe", |
| }, |
| "jaw": { |
| "wide": "wide jaw — soft layered styles reduce visual sharpness", |
| "narrow": "narrow jaw — side volume improves overall balance", |
| }, |
| "eyes": { |
| "wide": "wide-set eyes — vertical emphasis and clean partings suit you well", |
| "close": "close-set eyes — side width creates better visual spacing", |
| }, |
| "lips": { |
| "wide": "wider lips — soft texture on top balances the lower face", |
| "narrow": "narrower lips — clean structured styles complement well", |
| }, |
| "chin": { |
| "prominent": "prominent chin — textured top and length balance the profile", |
| "recessed": "recessed chin — volume on top draws focus upward", |
| }, |
| "symmetry": { |
| "high": "high facial symmetry — clean geometric styles suit you well", |
| "low": "noticeable asymmetry — textured styles redistribute visual balance", |
| }, |
| "eye_openness": { |
| "narrow": "narrower eyes — avoid heavy fringe to keep eyes visible", |
| }, |
| "thirds_vertical": { |
| "top_heavy": "forehead dominates — fringe and side volume balance the face", |
| "bottom_heavy": "lower face dominates — height on top corrects the balance", |
| }, |
| "hair_type": { |
| "curly": "your natural texture can work well with styles that embrace movement and volume", |
| "coily": "your natural texture can work well with rounded shape, controlled volume, and defined texture", |
| "straight": "clean and structured styles tend to complement your natural texture", |
| "wavy": "soft textured styles can enhance your natural movement", |
| }, |
| "hairline": { |
| "receding": "your hairline shape may work better with styles that avoid heavy forward fringe", |
| "uneven": "your hairline shape may benefit from softer texture and less rigid outlines", |
| }, |
| } |
|
|
| TRAIT_EXPLANATIONS_PL = { |
| "face_length": { |
| "long": "wydłużony kształt twarzy - objętość po bokach i grzywka pomagają zrównoważyć proporcje", |
| "short": "krótszy kształt twarzy - wysokość na górze pomaga optycznie wydłużyć proporcje", |
| "balanced": "długość twarzy jest dobrze zbalansowana", |
| }, |
|
|
| "forehead": { |
| "high": "wysokie czoło - grzywka pomaga optycznie obniżyć linię włosów", |
| "low": "niskie czoło - warto pozostawić czoło bardziej odkryte i unikać ciężkiej grzywki", |
| }, |
|
|
| "jaw": { |
| "wide": "szeroka szczęka - miękkie, warstwowe fryzury pomagają złagodzić jej optyczną szerokość", |
| "narrow": "wąska szczęka - objętość po bokach pomaga poprawić proporcje twarzy", |
| }, |
|
|
| "eyes": { |
| "wide": "szeroko rozstawione oczy - pionowe akcenty i uporządkowane przedziałki dobrze równoważą proporcje", |
| "close": "oczy blisko siebie - objętość po bokach pomaga stworzyć wrażenie większego odstępu", |
| }, |
|
|
| "lips": { |
| "wide": "szersze usta - lekka tekstura na górze pomaga zrównoważyć dolną część twarzy", |
| "narrow": "węższe usta - uporządkowane i strukturalne fryzury dobrze uzupełniają proporcje", |
| }, |
|
|
| "chin": { |
| "prominent": "wyraźny podbródek - tekstura na górze i odpowiednia długość pomagają zrównoważyć profil", |
| "recessed": "cofnięty podbródek - objętość na górze pomaga skierować uwagę wyżej", |
| }, |
|
|
| "symmetry": { |
| "high": "wysoka symetria twarzy - uporządkowane, geometryczne fryzury dobrze współgrają z proporcjami", |
| "low": "zauważalna asymetria - teksturowane fryzury pomagają rozłożyć uwagę i zrównoważyć twarz", |
| }, |
|
|
| "eye_openness": { |
| "narrow": "węższe oczy - warto unikać ciężkiej grzywki, aby nie zasłaniać oczu", |
| }, |
|
|
| "thirds_vertical": { |
| "top_heavy": "górna część twarzy dominuje - grzywka i objętość po bokach pomagają zrównoważyć proporcje", |
| "bottom_heavy": "dolna część twarzy dominuje - wysokość na górze pomaga poprawić balans", |
| }, |
|
|
| "hair_type": { |
| "curly": "naturalne loki dobrze współgrają z fryzurami wykorzystującymi ruch i objętość", |
| "coily": "naturalna struktura dobrze współgra z zaokrąglonym kształtem, kontrolowaną objętością i wyraźną teksturą", |
| "straight": "proste włosy dobrze współgrają z uporządkowanymi i strukturalnymi fryzurami", |
| "wavy": "delikatnie teksturowane fryzury mogą podkreślić naturalny ruch falowanych włosów", |
| }, |
|
|
| "hairline": { |
| "receding": "cofająca się linia włosów - lepiej sprawdzą się fryzury unikające ciężkiej grzywki zaczesanej do przodu", |
| "uneven": "nierówna linia włosów - dobrze zadziała lekka tekstura", |
| }, |
| } |
|
|
| MISSING_SENSITIVE_FEATURES = { |
| "volume_sides", |
| "fringe", |
| "curtain_fringe", |
| "layers", |
| "short_sides", |
| "longer_hair", |
| } |
|
|
| HAIR_TYPE_COMPATIBILITY = { |
| "straight": { |
| "Curly Volume": -0.3, |
| "Beach Waves": -0.15, |
| "Braided Crown": -0.1, |
| }, |
| "wavy": { |
| "Long Straight Blunt": -0.2, |
| "Bob Classic": -0.1, |
| }, |
| "curly": { |
| "Long Straight Blunt": -0.4, |
| "Slick Back": -0.3, |
| "Side Part": -0.2, |
| "Comb Over": -0.2, |
| "Pompadour": -0.15, |
| }, |
| "coily": { |
| "Long Straight Blunt": -0.5, |
| "Slick Back": -0.4, |
| "Side Part": -0.3, |
| "Bro Flow": -0.3, |
| "Pompadour": -0.2, |
| } |
| } |
|
|
| HAIRLINE_INCOMPATIBLE = { |
| "receding": [ |
| "French Crop", |
| "Textured Fringe", |
| "Curtain Fringe Medium", |
| "French Bob", |
| "Long with Curtain Fringe", |
| ], |
| } |
|
|
| def load_hairstyles(path="data/hairstyles.json"): |
| with open(path, "r") as f: |
| return json.load(f)["styles"] |
| |
| def compute_traits_influences(traits, gender): |
| from src.rules import apply_rules |
| base_scores = apply_rules(traits, gender=gender) |
| influences = {} |
|
|
| for key in traits: |
| if traits[key] in {None, "normal", "balanced", "slight_imbalance"}: |
| continue |
| traits_without = {**traits, key: "normal"} |
| scores_without = apply_rules(traits_without, gender=gender) |
| delta = { |
| dim: round(base_scores.get(dim, 0) - scores_without.get(dim, 0), 3) |
| for dim in base_scores |
| if abs(base_scores.get(dim, 0) - scores_without.get(dim, 0)) > 0.01 |
| } |
| total_impact = sum(abs(v) for v in delta.values()) |
| if total_impact > 0.5: |
| influences[key] = { |
| "value": traits[key], |
| "total_impact": round(total_impact, 3), |
| "delta": delta, |
| } |
| return dict(sorted( |
| influences.items(), |
| key=lambda x: x[1]["total_impact"], |
| reverse=True, |
| )) |
|
|
| def apply_hair_compatibility(score, style_name, traits): |
| hair_type = traits.get("hair_type") |
| hairline = traits.get("hairline") |
|
|
| if hair_type is not None and hair_type in HAIR_TYPE_COMPATIBILITY: |
| penalty = HAIR_TYPE_COMPATIBILITY[hair_type].get(style_name, 0) |
| score = score + penalty |
|
|
| if hairline == "receding" and style_name in HAIRLINE_INCOMPATIBLE["receding"]: |
| score = score - 0.4 |
|
|
| return max(0.0, score) |
|
|
| def score_hairstyle(user_scores, style, traits=None): |
| score = 0.0 |
| total_importance = 0.0 |
| matched_importance = 0.0 |
|
|
| for key, user_value in user_scores.items(): |
| style_value = style["attributes"].get(key, 0) |
| |
| importance = abs(user_value) |
| total_importance += importance |
|
|
| contribution = user_value * style_value |
| score += contribution |
| |
| if contribution > 0: |
| matched_importance += importance * style_value |
|
|
| if key in MISSING_SENSITIVE_FEATURES and user_value >= 3 and style_value < 0.2: |
| score -= user_value * 0.35 |
|
|
| if user_value <= -3 and style_value > 0.6: |
| score -= abs(user_value) * style_value * 0.35 |
| |
| if total_importance == 0: |
| return 0.0 |
| |
| base_score = score / total_importance |
| match_concentration = matched_importance / total_importance |
| final_score = base_score * (0.75 + 0.25 * match_concentration) |
|
|
| if traits: |
| final_score = apply_hair_compatibility( |
| final_score, style["name"], traits |
| ) |
|
|
| return final_score |
|
|
| def explain_match(user_scores, style, total_score, lang="pl"): |
| descriptions = STYLE_DESCRIPTIONS_PL if lang == "pl" else STYLE_DESCRIPTIONS |
| negatives_map = NEGATIVE_EXPLANATIONS_PL if lang == "pl" else NEGATIVE_EXPLANATIONS |
| positive = [] |
| negative = [] |
| missing = [] |
|
|
| pos_total = 0.0 |
| neg_total = 0.0 |
| missing_total = 0.0 |
|
|
| attributes = style.get("attributes", {}) |
|
|
| for key, user_value in user_scores.items(): |
| style_value = attributes.get(key, 0) |
| contribution = user_value * style_value |
|
|
| if contribution > 0: |
| positive.append({ |
| "feature": key, |
| "raw": contribution, |
| "desc": descriptions.get(key,key), |
| }) |
| pos_total += contribution |
| |
| elif contribution < 0: |
| negative.append({ |
| "feature": key, |
| "raw": contribution, |
| "desc": descriptions.get(key, key), |
| "reason": negatives_map.get( |
| key, |
| "może nie pasować do profilu Twojej twarzy" if lang == "pl" |
| else "may not suit your face profile" |
| ), |
| }) |
| neg_total += abs(contribution) |
| |
| if key in MISSING_SENSITIVE_FEATURES and user_value >= 3 and style_value < 0.2: |
| missing_strength = user_value * (1 - style_value) |
| feature_desc = descriptions.get(key, key) |
| if lang == "pl": |
| reason = ( |
| f"ten styl nie oferuje cechy „{feature_desc}”, " |
| f"którą Twoja analiza wyraźnie sugeruje" |
| ) |
| else: |
| reason = ( |
| f"this style lacks {feature_desc}, " |
| f"which your analysis strongly favours" |
| ) |
|
|
| missing.append({ |
| "feature": key, |
| "raw": missing_strength, |
| "desc": feature_desc, |
| "reason": reason, |
| }) |
| missing_total += missing_strength |
|
|
| for c in positive: |
| c["percent"] = c["raw"] / pos_total if pos_total > 0 else 0.0 |
| |
| for c in negative: |
| c["percent"] = abs(c["raw"]) / neg_total if neg_total > 0 else 0.0 |
|
|
| for c in missing: |
| c["percent"] = c["raw"] / missing_total if missing_total > 0 else 0.0 |
|
|
| positive.sort(key=lambda x: x["percent"], reverse=True) |
| negative.sort(key=lambda x: x["percent"], reverse=True) |
| missing.sort(key=lambda x: x["percent"], reverse=True) |
|
|
| return positive, negative, missing |
|
|
| def _build_face_analysis(influences, traits, lang="pl"): |
| explanations = [] |
| skip_values = { None, "normal", "balanced", "slight_imbalance"} |
| seen_dims = set() |
|
|
| priority_order = ["hairline", "hair_type"] + [ |
| k for k in influences.keys() if k not in ("hairline", "hair_type") |
| ] |
|
|
| for key in priority_order: |
| if key not in influences: |
| continue |
| info = influences[key] |
| value = info["value"] |
| if value in skip_values: |
| continue |
|
|
| exp = TRAIT_EXPLANATIONS.get(key, {}).get(value) |
| if not exp: |
| continue |
| delta = info["delta"] |
| top_dims = sorted(delta.items(), key=lambda x: abs(x[1]), reverse=True)[:2] |
| filtered_dims = [(d, c) for d, c in top_dims if d not in seen_dims] |
|
|
| if not filtered_dims and top_dims: |
| continue |
|
|
| dim_hints = [] |
| for dim, change in top_dims: |
| desc = STYLE_DESCRIPTIONS.get(dim, dim) |
| dim_hints.append(f"favours {desc}" if change > 0 else f"works against {desc}") |
| seen_dims.add(dim) |
| if dim_hints: |
| exp = f"{exp} ({', '.join(dim_hints)})" |
| explanations.append(exp) |
| |
| if len(explanations) >= 5: |
| break |
| |
| return explanations |
|
|
| def _build_face_analysis_llm(influences, traits, gender="Man", lang="pl"): |
| api_key = os.environ.get("GROQ_API_KEY") |
| if not api_key: |
| return _build_face_analysis(influences, traits) |
|
|
| trait_summary = _prepare_trait_summary(influences, traits, lang=lang) |
|
|
| if not trait_summary: |
| if lang == "pl": |
| return [ |
| "Proporcje Twojej twarzy są dobrze zbalansowane.", |
| "Większość fryzur powinna dobrze współgrać z Twoimi proporcjami." |
| ] |
| return [ |
| "Your facial proportions are well balanced.", |
| "Most hairstyles should work well with your proportions." |
| ] |
|
|
| gender_pl = "klientki" if gender == "Woman" else "klienta" |
| gender_en = "female client" if gender == "Woman" else "male client" |
|
|
| if lang == "pl": |
| system_msg = ( |
| f"Jesteś doświadczonym fryzjerem. Piszesz personalizowaną analizę twarzy {gender_pl}.\n" |
| "Zwracasz się bezpośrednio do klienta używając form: \"Twoja twarz\", \"dla Ciebie\", \"u Ciebie\".\n" |
| "Nigdy nie używaj: \"jego\", \"jej\", \"klient\", \"osoba\".\n" |
| "Piszesz naturalnie i ciepło — jak do kogoś kto siedzi przed Tobą w fotelu.\n" |
| "Odpowiadasz WYŁĄCZNIE w formacie JSON: {\"sentences\": [\"...\", \"...\", \"...\"]}" |
| ) |
| user_msg = ( |
| "Oto wykryte cechy twarzy wraz z ich wpływem na dobór fryzury:\n\n" |
| + "\n".join(trait_summary) |
| + "\n\n" |
| "Na podstawie tych cech napisz 3 zdania które:\n" |
| "1. opisują najważniejsze cechy twarzy klienta i co z nich wynika\n" |
| "2. wyjaśniają dlaczego konkretne kierunki fryzur będą korzystne\n" |
| "3. brzmią naturalnie i dają klientowi realną wartość\n\n" |
| "Każde zdanie maksymalnie 20 słów. Nie wymyślaj cech których nie ma w analizie.\n\n" |
| "Odpowiedź: {\"sentences\": [\"pierwsze zdanie.\", \"drugie zdanie.\", \"trzecie zdanie.\"]}" |
| ) |
| else: |
| system_msg = ( |
| f"You are an experienced hairstylist writing a personalised facial analysis for a {gender_en}.\n" |
| "Address the client directly using: \"your face\", \"for you\", \"your jawline\".\n" |
| "Never use: \"his\", \"her\", \"the client\", \"this person\".\n" |
| "Write warmly and naturally — as if the client is sitting in front of you.\n" |
| "Reply ONLY in JSON format: {\"sentences\": [\"...\", \"...\", \"...\"]}" |
| ) |
| user_msg = ( |
| "Detected facial features and their influence on hairstyle choice:\n\n" |
| + "\n".join(trait_summary) |
| + "\n\n" |
| "Based on these features, write 3 sentences that:\n" |
| "1. describe the most important facial characteristics and what they mean\n" |
| "2. explain why specific hairstyle directions will be beneficial\n" |
| "3. sound natural and give the client real, actionable insight\n\n" |
| "Max 20 words per sentence. Do not invent features not present in the analysis.\n\n" |
| "Response: {\"sentences\": [\"first sentence.\", \"second sentence.\", \"third sentence.\"]}" |
| ) |
|
|
| try: |
| client = Groq(api_key=api_key) |
| response = client.chat.completions.create( |
| model = "qwen/qwen3.6-27b", |
| max_tokens = 400, |
| temperature = 0.7, |
| top_p = 0.80, |
| reasoning_effort="none", |
| messages = [ |
| {"role": "system", "content": system_msg}, |
| {"role": "user", "content": user_msg}, |
| ], |
| ) |
|
|
| import json |
| import re |
| text = response.choices[0].message.content.strip() |
| print(f"DEBUG LLM response: {text!r}") |
| if not text.endswith('}'): |
| matches = re.findall(r'"([^"]*)"', text) |
| if matches: |
| sentences = [m for m in matches if len(m) > 10] |
| if sentences: |
| return sentences[:4] |
| try: |
| parsed = json.loads(text) |
| except json.JSONDecodeError: |
| for suffix in [']}', '}', ']']: |
| try: |
| parsed = json.loads(text + suffix) |
| break |
| except json.JSONDecodeError: |
| continue |
| else: |
| print(f"LLM JSON parse failed: {text!r}") |
| return _build_face_analysis(influences, traits) |
|
|
| if isinstance(parsed, dict) and "sentences" in parsed: |
| sentences = parsed["sentences"] |
| elif isinstance(parsed, list): |
| sentences = parsed |
| else: |
| print(f"DEBUG unexpected structure: {parsed}") |
| return _build_face_analysis(influences, traits) |
|
|
| if isinstance(sentences, list) and all(isinstance(s, str) for s in sentences): |
| return sentences[:4] |
|
|
| except Exception as e: |
| print(f"LLM error: {e}") |
|
|
| return _build_face_analysis(influences, traits) |
|
|
| def _prepare_trait_summary(influences, traits, lang="pl"): |
| skip_values = {None, "normal", "balanced", "slight_imbalance"} |
| descriptions = STYLE_DESCRIPTIONS_PL if lang == "pl" else STYLE_DESCRIPTIONS |
| explanations = TRAIT_EXPLANATIONS_PL if lang == "pl" else TRAIT_EXPLANATIONS |
| favours_word = "sprzyja" if lang == "pl" else "favours" |
| against_word = "utrudnia" if lang == "pl" else "works against" |
|
|
| trait_summary = [] |
| priority_order = ["hairline", "hair_type"] + [ |
| k for k in influences.keys() |
| if k not in ("hairline", "hair_type") |
| ] |
|
|
| for key in priority_order[:6]: |
| if key not in influences: |
| continue |
| info = influences[key] |
| value = info["value"] |
| if value in skip_values: |
| continue |
|
|
| delta = info["delta"] |
| top_dims = sorted(delta.items(), |
| key=lambda x: abs(x[1]), reverse=True)[:2] |
| hints = [] |
| for dim, change in top_dims: |
| desc = descriptions.get(dim, dim) |
| hints.append( |
| f"{favours_word} {desc}" if change > 0 |
| else f"{against_word} {desc}" |
| ) |
|
|
| trait_label = explanations.get(key, {}).get(value) or f"{key}: {value}" |
|
|
| trait_summary.append( |
| f"- {trait_label}" |
| + (f" ({', '.join(hints)})" if hints else "") |
| ) |
|
|
| return trait_summary |
|
|
| def _build_style_result(style, user_scores, traits, score, lang): |
| positive, negative, missing = explain_match( |
| user_scores, |
| style, |
| score, |
| lang=lang |
| ) |
|
|
| return { |
| "name": style["name"], |
| "score": score, |
| "category": style.get("category", ""), |
| "tags": style.get(f"tags_{lang}", style.get("tags", [])), |
| "description": style.get( |
| f"description_{lang}", |
| style.get("description", "") |
| ), |
| "contributions": positive, |
| "negatives": negative, |
| "missing": missing, |
| "image": style.get("image"), |
| } |
|
|
| def normalize_scores_for_display(results): |
| if not results: |
| return results |
| raw = [r["score"] for r in results] |
| max_s = max(raw) |
| min_s = min(raw) |
| rng = max_s - min_s if max_s != min_s else 1.0 |
|
|
| for r in results: |
| normalized = 50 + ((r["score"] - min_s) / rng) * 49 |
| r["display_score"] = round(normalized) |
| return results |
|
|
| def generate_recommendations(user_scores, traits, gender="Man", top_k=3, |
| hairstyles_path="data/hairstyles.json", lang="pl"): |
| styles = load_hairstyles(hairstyles_path) |
| influences = compute_traits_influences(traits, gender) |
| print(f"DEBUG influences: {list(influences.keys())}") |
| results_pl = [] |
| results_en = [] |
|
|
| for style in styles: |
| score = score_hairstyle(user_scores, style, traits) |
|
|
| results_pl.append( |
| _build_style_result( |
| style, |
| user_scores, |
| traits, |
| score, |
| lang="pl" |
| ) |
| ) |
|
|
| results_en.append( |
| _build_style_result( |
| style, |
| user_scores, |
| traits, |
| score, |
| lang="en" |
| ) |
| ) |
|
|
| results_pl.sort(key=lambda x: x["score"], reverse=True) |
| results_en.sort(key=lambda x: x["score"], reverse=True) |
|
|
| results_pl = normalize_scores_for_display(results_pl) |
| results_en = normalize_scores_for_display(results_en) |
|
|
| return { |
| "top_styles": { |
| "pl": results_pl[:top_k], |
| "en": results_en[:top_k], |
| }, |
|
|
| "all_styles": { |
| "pl": results_pl, |
| "en": results_en, |
| }, |
|
|
| "face_analysis": { |
| "pl": _build_face_analysis_llm( |
| influences, traits, gender, lang="pl" |
| ), |
| "en": _build_face_analysis_llm( |
| influences, traits, gender, lang="en" |
| ), |
| }, |
|
|
| "trait_influences": influences, |
| } |