From 921dfd55f4692aefcdd38e20c6986922d5c62d89 Mon Sep 17 00:00:00 2001 From: Chaewon Lee Date: Sat, 13 Jun 2026 01:47:11 +0900 Subject: [PATCH] feat: expand flower database and sync bouquet recipe prompts * feat: expand flower database and improve mood matching * feat: sync bouquet recipe prompt with flower candidate metadata --- src/lib/server/flowerFlow/flowerDB.js | 322 +++++++++++++++++++------- src/lib/server/gemini/text.js | 23 +- 2 files changed, 251 insertions(+), 94 deletions(-) diff --git a/src/lib/server/flowerFlow/flowerDB.js b/src/lib/server/flowerFlow/flowerDB.js index 0f56796..dcf3345 100644 --- a/src/lib/server/flowerFlow/flowerDB.js +++ b/src/lib/server/flowerFlow/flowerDB.js @@ -2,6 +2,7 @@ /** * @typedef {Object} FlowerRecord + * @property {number} id * @property {string} name * @property {string[]} colors * @property {string[]} season @@ -9,96 +10,200 @@ * @property {string[]} meanings * @property {'low' | 'medium' | 'high'} priceLevel * @property {string[]} mood - * @property {'main' | 'sub' | 'greenery'} role + * @property {'main' | 'filler' | 'line' | 'foliage'} role + * @property {string} [family] Botanical grouping — used to avoid duplicate substitutes in one recipe + * @property {'common' | 'limited' | 'rare'} [cutAvailability] How often this appears as cut flower in Korean florists */ +/** Neutral tones that mood analysis palettes rarely name directly. */ +const NEUTRAL_COLORS = new Set(['silver', 'beige', 'brown']); + +/** @type {Record} */ +const NEUTRAL_COLOR_ALIASES = { + silver: ['white', 'grey', 'gray', 'muted', 'neutral', 'cool', 'pale', 'soft'], + beige: ['cream', 'warm', 'neutral', 'natural', 'taupe', 'sand', 'ivory', 'tan', 'earth'], + brown: ['earth', 'natural', 'rustic', 'warm', 'neutral', 'organic', 'mocha', 'wood', 'tan'] +}; + +/** @type {Record} */ +const CUT_AVAILABILITY_PENALTY = { + common: 0, + limited: 1, + rare: 2 +}; + /** @type {FlowerRecord[]} */ export const flowerDB = [ - { - name: 'Tulip', - colors: ['white', 'pink', 'yellow', 'purple'], - season: ['spring'], - wordOfFlower: 'confession of love', - meanings: ['love', 'passion', 'devotion'], - priceLevel: 'medium', - mood: ['soft', 'romantic', 'clean'], - role: 'main' - }, - { - name: 'Gerbera', - colors: ['white', 'pink', 'yellow', 'orange', 'red'], - season: ['spring', 'summer'], - wordOfFlower: 'cheerfulness', - meanings: ['joy', 'friendship', 'warmth'], - priceLevel: 'low', - mood: ['bright', 'playful', 'cheerful'], - role: 'main' - }, - { - name: "Baby's breath", - colors: ['white', 'pink'], - season: ['spring', 'summer', 'autumn'], - wordOfFlower: 'pure heart', - meanings: ['innocence', 'everlasting love'], - priceLevel: 'low', - mood: ['airy', 'delicate', 'soft'], - role: 'sub' - }, - { - name: 'Rose', - colors: ['white', 'pink', 'red', 'peach'], - season: ['spring', 'summer', 'autumn'], - wordOfFlower: 'love', - meanings: ['romance', 'gratitude', 'elegance'], - priceLevel: 'medium', - mood: ['romantic', 'classic', 'warm'], - role: 'main' - }, - { - name: 'Ranunculus', - colors: ['white', 'pink', 'peach', 'yellow'], - season: ['spring'], - wordOfFlower: 'charm', - meanings: ['radiance', 'charm', 'attraction'], - priceLevel: 'medium', - mood: ['soft', 'layered', 'romantic'], - role: 'main' - }, - { - name: 'Eucalyptus', - colors: ['green', 'silver green'], - season: ['spring', 'summer', 'autumn', 'winter'], - wordOfFlower: 'protection', - meanings: ['freshness', 'healing'], - priceLevel: 'low', - mood: ['natural', 'minimal', 'clean'], - role: 'greenery' - }, - { - name: 'Lisianthus', - colors: ['white', 'pink', 'purple', 'green'], - season: ['summer', 'autumn'], - wordOfFlower: 'gratitude', - meanings: ['appreciation', 'grace'], - priceLevel: 'medium', - mood: ['elegant', 'soft', 'delicate'], - role: 'sub' - }, - { - name: 'Daisy', - colors: ['white', 'yellow'], - season: ['spring', 'summer'], - wordOfFlower: 'innocence', - meanings: ['purity', 'new beginnings'], - priceLevel: 'low', - mood: ['fresh', 'natural', 'cheerful'], - role: 'sub' - } + // ── Focal blooms ──────────────────────────────────────────── + { id: 1, name: 'Daffodil', colors: ['yellow', 'white'], season: ['spring'], wordOfFlower: 'self-love', meanings: ['self-love', 'pride', 'vanity'], priceLevel: 'low', mood: ['bright', 'fresh', 'cheerful'], role: 'main' }, + { id: 2, name: 'Stock', colors: ['purple', 'pink', 'white'], season: ['spring'], wordOfFlower: 'lasting beauty', meanings: ['lasting beauty', 'bonds of love'], priceLevel: 'medium', mood: ['soft', 'romantic', 'fragrant'], role: 'main' }, + { id: 3, name: 'Amaryllis', colors: ['red', 'white', 'pink'], season: ['winter'], wordOfFlower: 'dazzling beauty', meanings: ['dazzling beauty', 'talkativeness'], priceLevel: 'medium', mood: ['bold', 'dramatic', 'festive'], role: 'main' }, + { id: 4, name: 'Sweet Pea', colors: ['pink', 'purple', 'white'], season: ['spring'], wordOfFlower: 'delicate pleasures', meanings: ['joy', 'daintiness'], priceLevel: 'medium', mood: ['soft', 'romantic', 'fragrant'], role: 'main' }, + { id: 5, name: 'Anthurium', colors: ['red', 'pink', 'white'], season: ['summer'], wordOfFlower: 'hospitality', meanings: ['hospitality', 'passion'], priceLevel: 'medium', mood: ['exotic', 'bold', 'tropical'], role: 'main' }, + { id: 6, name: 'Freesia', colors: ['yellow', 'white', 'purple'], season: ['spring'], wordOfFlower: 'purity', meanings: ['purity', 'innocent heart'], priceLevel: 'medium', mood: ['fresh', 'fragrant', 'cheerful'], role: 'main' }, + { id: 7, name: 'Tulip', colors: ['red', 'yellow', 'pink', 'white', 'purple'], season: ['spring'], wordOfFlower: 'benevolence', meanings: ['benevolence', 'fame', 'honor'], priceLevel: 'medium', mood: ['soft', 'romantic', 'clean'], role: 'main' }, + { id: 8, name: 'Hyacinth', colors: ['purple', 'pink', 'blue', 'white'], season: ['spring'], wordOfFlower: 'joy of the heart', meanings: ['joy', 'victory'], priceLevel: 'medium', mood: ['fragrant', 'soft', 'romantic'], role: 'main' }, + { id: 9, name: 'Ranunculus', colors: ['pink', 'yellow', 'orange', 'white'], season: ['spring'], wordOfFlower: 'radiant charm', meanings: ['radiant charm', 'reproach'], priceLevel: 'medium', mood: ['soft', 'romantic', 'cheerful'], role: 'main' }, + { id: 10, name: 'Lilac', colors: ['purple', 'white'], season: ['spring'], wordOfFlower: 'memories of youth', meanings: ['young love', 'memories'], priceLevel: 'medium', mood: ['fragrant', 'romantic', 'soft'], role: 'main' }, + { id: 11, name: 'Iris', colors: ['purple', 'blue', 'white'], season: ['spring'], wordOfFlower: 'good news', meanings: ['good news', 'capriciousness'], priceLevel: 'medium', mood: ['elegant', 'fresh', 'noble'], role: 'main' }, + { id: 12, name: 'Tree Peony', colors: ['red', 'pink', 'white', 'purple'], season: ['spring'], wordOfFlower: 'wealth and honor', meanings: ['wealth', 'glory', 'sincerity'], priceLevel: 'high', mood: ['luxurious', 'romantic', 'elegant'], role: 'main' }, + { id: 13, name: 'Peony', colors: ['pink', 'white', 'red'], season: ['spring', 'summer'], wordOfFlower: 'shyness', meanings: ['shyness', 'bashfulness'], priceLevel: 'high', mood: ['romantic', 'soft', 'luxurious'], role: 'main' }, + { id: 14, name: 'Rose', colors: ['red', 'pink', 'white', 'yellow'], season: ['spring', 'summer'], wordOfFlower: 'passionate love', meanings: ['passionate love', 'jealousy', 'purity'], priceLevel: 'medium', mood: ['romantic', 'elegant', 'bold'], role: 'main' }, + { id: 15, name: 'Snowball Viburnum', colors: ['white', 'green'], season: ['spring'], wordOfFlower: 'grace', meanings: ['grace', 'giving'], priceLevel: 'medium', mood: ['soft', 'fresh', 'elegant'], role: 'main', cutAvailability: 'limited' }, + { id: 16, name: 'Carnation', colors: ['pink', 'red', 'white'], season: ['spring', 'summer'], wordOfFlower: "a woman's affection", meanings: ['affection', "mother's love"], priceLevel: 'low', mood: ['warm', 'soft', 'romantic'], role: 'main' }, + { id: 17, name: 'Clematis', colors: ['purple', 'white', 'pink'], season: ['summer'], wordOfFlower: 'nobility', meanings: ['nobility', 'beautiful heart'], priceLevel: 'medium', mood: ['elegant', 'soft', 'romantic'], role: 'main' }, + { id: 18, name: 'Lily', colors: ['white', 'pink', 'orange'], season: ['summer'], wordOfFlower: 'purity', meanings: ['purity', 'pure heart'], priceLevel: 'medium', mood: ['elegant', 'fragrant', 'clean'], role: 'main' }, + { id: 19, name: 'Hydrangea', colors: ['blue', 'pink', 'purple', 'white'], season: ['summer'], wordOfFlower: 'heartlessness', meanings: ['coldness', 'heartlessness', 'pride'], priceLevel: 'medium', mood: ['soft', 'elegant', 'lush'], role: 'main' }, + { id: 20, name: 'Agapanthus', colors: ['blue', 'purple', 'white'], season: ['summer'], wordOfFlower: 'love letter', meanings: ['love letter', 'news of love'], priceLevel: 'medium', mood: ['elegant', 'fresh', 'serene'], role: 'main' }, + { id: 21, name: 'Allium', colors: ['purple'], season: ['spring', 'summer'], wordOfFlower: 'endless sorrow', meanings: ['endless sorrow', 'prosperity'], priceLevel: 'medium', mood: ['bold', 'elegant', 'dramatic'], role: 'main' }, + { id: 22, name: 'Bellflower (Campanula)', colors: ['blue', 'purple', 'white'], season: ['summer'], wordOfFlower: 'a coquettish look', meanings: ['gratitude', 'charm'], priceLevel: 'low', mood: ['soft', 'delicate', 'serene'], role: 'main' }, + { id: 23, name: 'China Aster (Callistephus)', colors: ['purple', 'pink', 'white', 'red'], season: ['summer', 'autumn'], wordOfFlower: 'trusting love', meanings: ['trusting love', 'memories'], priceLevel: 'low', mood: ['soft', 'cheerful', 'vintage'], role: 'main' }, + { id: 24, name: 'Poppy (Papaver)', colors: ['red', 'pink', 'orange', 'white'], season: ['spring', 'summer'], wordOfFlower: 'consolation', meanings: ['consolation', 'comfort'], priceLevel: 'medium', mood: ['soft', 'delicate', 'whimsical'], role: 'main' }, + { id: 25, name: 'Dahlia', colors: ['red', 'pink', 'orange', 'white', 'purple'], season: ['summer', 'autumn'], wordOfFlower: 'gratitude', meanings: ['gratitude', 'elegance'], priceLevel: 'medium', mood: ['bold', 'elegant', 'luxurious'], role: 'main' }, + { id: 26, name: 'Lotus', colors: ['pink', 'white'], season: ['summer'], wordOfFlower: 'purity', meanings: ['purity', 'divinity', 'detachment'], priceLevel: 'medium', mood: ['serene', 'elegant', 'clean'], role: 'main', cutAvailability: 'rare' }, + { id: 27, name: 'Gentian', colors: ['blue', 'purple'], season: ['autumn'], wordOfFlower: "I love you when you're sad", meanings: ['love in sadness', 'sincerity'], priceLevel: 'medium', mood: ['serene', 'elegant', 'calm'], role: 'main' }, + { id: 28, name: 'Sunflower', colors: ['yellow', 'orange'], season: ['summer'], wordOfFlower: 'adoration', meanings: ['longing', 'adoration'], priceLevel: 'low', mood: ['bright', 'cheerful', 'warm'], role: 'main' }, + { id: 29, name: 'Chrysanthemum', colors: ['white', 'yellow', 'pink', 'purple'], season: ['autumn'], wordOfFlower: 'purity', meanings: ['purity', 'chastity', 'innocence'], priceLevel: 'low', mood: ['elegant', 'calm', 'vintage'], role: 'main' }, + { id: 30, name: 'Cockscomb (Celosia)', colors: ['red', 'orange', 'pink', 'yellow'], season: ['summer', 'autumn'], wordOfFlower: 'ardent love', meanings: ['ardent love', 'vain ornament'], priceLevel: 'low', mood: ['bold', 'warm', 'vintage'], role: 'main' }, + { id: 31, name: 'Anemone', colors: ['red', 'purple', 'white', 'pink'], season: ['spring'], wordOfFlower: 'sincerity', meanings: ['solitude', 'chastity', 'sincerity'], priceLevel: 'medium', mood: ['soft', 'elegant', 'romantic'], role: 'main' }, + { id: 32, name: 'Cosmos', colors: ['pink', 'white', 'purple'], season: ['autumn'], wordOfFlower: "a girl's pure heart", meanings: ['pure heart', 'innocence'], priceLevel: 'low', mood: ['soft', 'delicate', 'wild'], role: 'main' }, + { id: 33, name: 'Red Spider Lily (Lycoris radiata)', colors: ['red'], season: ['autumn'], wordOfFlower: 'true love', meanings: ['true love', 'longing'], priceLevel: 'medium', mood: ['dramatic', 'bold', 'elegant'], role: 'main' }, + { id: 34, name: 'Gerbera', colors: ['white', 'pink', 'yellow', 'orange', 'red'], season: ['spring', 'summer'], wordOfFlower: 'mystery', meanings: ['mystery', 'cheerfulness'], priceLevel: 'low', mood: ['bright', 'playful', 'cheerful'], role: 'main' }, + { id: 35, name: 'Calla Lily', colors: ['white', 'yellow', 'pink', 'purple'], season: ['spring', 'summer'], wordOfFlower: 'joy', meanings: ['joy', 'passion'], priceLevel: 'medium', mood: ['elegant', 'clean', 'refined'], role: 'main' }, + { id: 36, name: 'Bird of Paradise (Strelitzia)', colors: ['orange', 'blue'], season: ['winter'], wordOfFlower: 'mystery', meanings: ['mystery', 'magnificence'], priceLevel: 'high', mood: ['exotic', 'bold', 'dramatic'], role: 'main' }, + { id: 37, name: 'Hellebore', colors: ['white', 'green', 'purple', 'pink'], season: ['winter'], wordOfFlower: 'reason for being', meanings: ['serenity', 'reason for being'], priceLevel: 'medium', mood: ['elegant', 'serene', 'soft'], role: 'main' }, + + // ── Florist staples (common in shops; not on the NIHHS list) ─ + { id: 38, name: 'Lisianthus (Eustoma)', colors: ['white', 'pink', 'purple', 'green'], season: ['summer'], wordOfFlower: 'appreciation', meanings: ['appreciation', 'calm', 'gratitude'], priceLevel: 'medium', mood: ['soft', 'elegant', 'romantic'], role: 'main' }, + { id: 39, name: 'Scabiosa', colors: ['purple', 'pink', 'white', 'blue'], season: ['summer'], wordOfFlower: 'I have lost all', meanings: ['admiration', 'unlucky love', 'purity'], priceLevel: 'low', mood: ['delicate', 'soft', 'whimsical'], role: 'filler' }, + { id: 40, name: 'Wax Flower (Chamelaucium)', colors: ['pink', 'white', 'purple'], season: ['spring', 'winter'], wordOfFlower: 'lasting love', meanings: ['lasting love', 'patience'], priceLevel: 'low', mood: ['delicate', 'fresh', 'rustic'], role: 'filler' }, + { id: 41, name: 'Caspia (Limonium)', colors: ['purple', 'blue'], season: ['summer'], wordOfFlower: 'remembrance', meanings: ['remembrance', 'success'], priceLevel: 'low', mood: ['airy', 'delicate', 'rustic'], role: 'filler', family: 'limonium' }, + { id: 42, name: 'Ruscus', colors: ['green'], season: ['spring', 'summer', 'autumn', 'winter'], wordOfFlower: 'endurance', meanings: ['protection', 'endurance'], priceLevel: 'low', mood: ['fresh', 'lush', 'evergreen'], role: 'foliage' }, + { id: 43, name: 'Veronica (Speedwell)', colors: ['purple', 'blue', 'white', 'pink'], season: ['summer'], wordOfFlower: 'fidelity', meanings: ['fidelity', 'loyalty'], priceLevel: 'low', mood: ['fresh', 'soft', 'serene'], role: 'line' }, + { id: 44, name: 'Solidago (Goldenrod)', colors: ['yellow'], season: ['summer', 'autumn'], wordOfFlower: 'encouragement', meanings: ['encouragement', 'good fortune'], priceLevel: 'low', mood: ['bright', 'cheerful', 'rustic'], role: 'filler' }, + { id: 45, name: 'Bouvardia', colors: ['red', 'pink', 'white'], season: ['summer', 'autumn'], wordOfFlower: 'enthusiasm', meanings: ['enthusiasm', 'zest'], priceLevel: 'medium', mood: ['soft', 'elegant', 'delicate'], role: 'filler' }, + { id: 46, name: 'Tweedia (Oxypetalum)', colors: ['blue'], season: ['summer'], wordOfFlower: 'believe in me', meanings: ['trust', 'mutual love'], priceLevel: 'medium', mood: ['soft', 'serene', 'delicate'], role: 'filler' }, + { id: 47, name: 'Craspedia (Billy Balls)', colors: ['yellow'], season: ['summer'], wordOfFlower: 'good health', meanings: ['good health', 'individuality'], priceLevel: 'medium', mood: ['playful', 'cheerful', 'modern'], role: 'filler' }, + { id: 48, name: 'Amaranthus', colors: ['red', 'green'], season: ['summer', 'autumn'], wordOfFlower: 'immortality', meanings: ['immortality', 'hopeless love'], priceLevel: 'medium', mood: ['bold', 'dramatic', 'lush'], role: 'line' }, + { id: 49, name: "Queen Anne's Lace (Ammi)", colors: ['white'], season: ['summer'], wordOfFlower: 'sanctuary', meanings: ['delicacy', 'sanctuary'], priceLevel: 'low', mood: ['airy', 'delicate', 'soft'], role: 'filler' }, + { id: 50, name: 'Nigella (Love-in-a-mist)', colors: ['blue', 'white', 'purple'], season: ['summer'], wordOfFlower: 'perplexity', meanings: ['delicate ties', 'perplexity'], priceLevel: 'low', mood: ['delicate', 'dreamy', 'soft'], role: 'filler' }, + { id: 51, name: 'Brunia', colors: ['green', 'silver'], season: ['winter'], wordOfFlower: 'unity', meanings: ['protection', 'unity'], priceLevel: 'medium', mood: ['modern', 'textured', 'rustic'], role: 'filler' }, + + // ── Line / spike flowers ──────────────────────────────────── + { id: 52, name: 'Snapdragon', colors: ['pink', 'yellow', 'red', 'white'], season: ['spring', 'summer'], wordOfFlower: 'desire', meanings: ['desire', 'presumption'], priceLevel: 'medium', mood: ['bright', 'playful', 'bold'], role: 'line' }, + { id: 53, name: 'Lupine', colors: ['purple', 'pink', 'blue'], season: ['spring', 'summer'], wordOfFlower: 'lust for life', meanings: ['lust for life', 'imagination'], priceLevel: 'medium', mood: ['bold', 'fresh', 'wild'], role: 'line' }, + { id: 54, name: 'Gladiolus', colors: ['red', 'pink', 'white', 'yellow'], season: ['summer'], wordOfFlower: 'secret meeting', meanings: ['secret meeting', 'caution'], priceLevel: 'medium', mood: ['bold', 'dramatic', 'elegant'], role: 'line' }, + { id: 55, name: 'Foxglove (Digitalis)', colors: ['purple', 'pink', 'white'], season: ['summer'], wordOfFlower: 'ardent love', meanings: ['ardent love', 'insincerity', 'splendor'], priceLevel: 'medium', mood: ['dramatic', 'elegant', 'bold'], role: 'line' }, + { id: 56, name: 'Delphinium', colors: ['blue', 'purple', 'white'], season: ['summer'], wordOfFlower: 'understand my heart', meanings: ['lightness', 'rashness', 'understanding'], priceLevel: 'medium', mood: ['fresh', 'elegant', 'serene'], role: 'line' }, + { id: 57, name: 'Salvia (Scarlet Sage)', colors: ['red', 'purple'], season: ['summer', 'autumn'], wordOfFlower: 'burning heart', meanings: ['burning heart', 'passion'], priceLevel: 'low', mood: ['bold', 'warm', 'vibrant'], role: 'line' }, + + // ── Filler & accent ───────────────────────────────────────── + { id: 58, name: 'Grape Hyacinth (Muscari)', colors: ['blue', 'purple'], season: ['spring'], wordOfFlower: 'disappointment', meanings: ['disappointment', 'despair'], priceLevel: 'low', mood: ['delicate', 'soft', 'fresh'], role: 'filler' }, + { id: 59, name: 'Forget-me-not', colors: ['blue'], season: ['spring'], wordOfFlower: 'true love', meanings: ['true love', 'forget me not'], priceLevel: 'low', mood: ['delicate', 'soft', 'romantic'], role: 'filler' }, + { id: 60, name: 'Statice (Limonium)', colors: ['purple', 'pink', 'white', 'yellow'], season: ['summer'], wordOfFlower: 'eternal love', meanings: ['eternal love', 'remembrance'], priceLevel: 'low', mood: ['delicate', 'rustic', 'vintage'], role: 'filler', family: 'limonium' }, + { id: 61, name: 'Astilbe', colors: ['pink', 'white', 'red'], season: ['summer'], wordOfFlower: 'bashfulness', meanings: ['bashfulness', 'I will be waiting'], priceLevel: 'medium', mood: ['soft', 'delicate', 'romantic'], role: 'filler' }, + { id: 62, name: 'Strawflower (Helichrysum)', colors: ['yellow', 'orange', 'pink', 'red'], season: ['summer'], wordOfFlower: 'always remember', meanings: ['remembrance', 'immortality'], priceLevel: 'low', mood: ['warm', 'vintage', 'rustic'], role: 'filler' }, + { id: 63, name: 'Cornflower (Centaurea)', colors: ['blue', 'purple', 'pink'], season: ['summer'], wordOfFlower: 'happiness', meanings: ['happiness', 'delicacy', 'cheer'], priceLevel: 'low', mood: ['fresh', 'cheerful', 'soft'], role: 'filler' }, + { id: 64, name: 'Chinese Lantern (Physalis)', colors: ['orange'], season: ['autumn'], wordOfFlower: 'falsehood', meanings: ['falsehood', 'deception'], priceLevel: 'low', mood: ['warm', 'rustic', 'vintage'], role: 'filler' }, + { id: 65, name: 'Globe Amaranth (Gomphrena)', colors: ['purple', 'pink', 'white'], season: ['summer', 'autumn'], wordOfFlower: 'immortality', meanings: ['immortality', 'constancy'], priceLevel: 'low', mood: ['vintage', 'cheerful', 'rustic'], role: 'filler' }, + { id: 66, name: 'Showy Stonecrop (Sedum)', colors: ['pink', 'red'], season: ['autumn'], wordOfFlower: 'hope', meanings: ['hope', 'life'], priceLevel: 'low', mood: ['soft', 'rustic', 'calm'], role: 'filler' }, + { id: 67, name: "Baby's Breath (Gypsophila)", colors: ['white', 'pink'], season: ['summer'], wordOfFlower: 'earnest joy', meanings: ['earnest joy', 'pure heart'], priceLevel: 'low', mood: ['soft', 'clean', 'delicate'], role: 'filler' }, + + // ── Orchids (cut) ─────────────────────────────────────────── + { id: 68, name: 'Cattleya', colors: ['purple', 'pink', 'white'], season: ['autumn'], wordOfFlower: 'you are a beauty', meanings: ['beauty', 'elegance'], priceLevel: 'high', mood: ['elegant', 'exotic', 'luxurious'], role: 'main' }, + { id: 69, name: 'Oncidium', colors: ['yellow'], season: ['autumn'], wordOfFlower: 'innocent heart', meanings: ['innocence', 'simplicity'], priceLevel: 'medium', mood: ['playful', 'bright', 'delicate'], role: 'filler' }, + { id: 70, name: 'Dendrobium', colors: ['white', 'purple', 'pink'], season: ['winter'], wordOfFlower: 'a beauty', meanings: ['tomboy', 'beauty'], priceLevel: 'medium', mood: ['elegant', 'exotic', 'refined'], role: 'main' }, + { id: 71, name: 'Vanda Orchid', colors: ['purple', 'blue', 'pink'], season: ['summer'], wordOfFlower: 'a token of affection', meanings: ['affection', 'elegance'], priceLevel: 'high', mood: ['exotic', 'luxurious', 'elegant'], role: 'main' }, + + // ── Foliage, grasses & berries ────────────────────────────── + { id: 72, name: 'Holly', colors: ['green', 'red'], season: ['winter'], wordOfFlower: 'protection', meanings: ['protection', 'defense'], priceLevel: 'low', mood: ['festive', 'evergreen', 'fresh'], role: 'foliage' }, + { id: 73, name: 'Nandina (Heavenly Bamboo)', colors: ['red', 'green'], season: ['winter'], wordOfFlower: 'enduring love', meanings: ['enduring love', 'devotion'], priceLevel: 'low', mood: ['festive', 'evergreen', 'warm'], role: 'foliage' }, + { id: 74, name: 'Pussy Willow (Salix gracilistyla)', colors: ['silver', 'green'], season: ['spring'], wordOfFlower: 'freedom', meanings: ['freedom', 'kindness'], priceLevel: 'low', mood: ['soft', 'fresh', 'rustic'], role: 'line' }, + { id: 75, name: 'Maidenhair Fern (Adiantum)', colors: ['green'], season: ['summer'], wordOfFlower: 'charm', meanings: ['charm', 'grace'], priceLevel: 'low', mood: ['delicate', 'fresh', 'soft'], role: 'foliage' }, + { id: 76, name: 'Cotton', colors: ['white'], season: ['autumn'], wordOfFlower: "mother's love", meanings: ["mother's love", 'nobility'], priceLevel: 'low', mood: ['soft', 'cozy', 'rustic'], role: 'filler' }, + { id: 77, name: 'Hosta (Plantain Lily)', colors: ['white', 'purple', 'green'], season: ['summer'], wordOfFlower: 'composure', meanings: ['composure', 'quietness'], priceLevel: 'low', mood: ['calm', 'lush', 'serene'], role: 'foliage' }, + { id: 78, name: 'Beautyberry (Callicarpa)', colors: ['purple'], season: ['autumn'], wordOfFlower: 'intelligence', meanings: ['intelligence', 'wit'], priceLevel: 'low', mood: ['vibrant', 'rustic', 'elegant'], role: 'foliage' }, + { id: 79, name: 'Reed', colors: ['brown', 'green', 'beige'], season: ['autumn'], wordOfFlower: 'faith', meanings: ['faith', 'trust', 'wisdom'], priceLevel: 'low', mood: ['rustic', 'calm', 'wild'], role: 'line' }, + { id: 80, name: 'Foxtail Millet', colors: ['yellow', 'green'], season: ['autumn'], wordOfFlower: 'equality', meanings: ['equality', 'abundance'], priceLevel: 'low', mood: ['rustic', 'warm', 'wild'], role: 'line' }, + { id: 81, name: 'Mint', colors: ['purple', 'white', 'green'], season: ['summer'], wordOfFlower: 'virtue', meanings: ['virtue', 'warmth'], priceLevel: 'low', mood: ['fresh', 'fragrant', 'wild'], role: 'foliage' }, + { id: 82, name: 'Asparagus Fern', colors: ['green'], season: ['summer'], wordOfFlower: 'constancy', meanings: ['constancy', 'devotion'], priceLevel: 'low', mood: ['fresh', 'delicate', 'lush'], role: 'foliage' }, + { id: 83, name: 'Silver Grass (Miscanthus)', colors: ['silver', 'beige'], season: ['autumn'], wordOfFlower: 'retirement', meanings: ['retreat', 'serenity'], priceLevel: 'low', mood: ['rustic', 'calm', 'wild'], role: 'line' }, + { id: 84, name: 'Eucalyptus', colors: ['green', 'silver'], season: ['winter'], wordOfFlower: 'memories', meanings: ['memories', 'protection'], priceLevel: 'low', mood: ['fresh', 'fragrant', 'calm'], role: 'foliage' }, + { id: 85, name: 'Ivy (Hedera)', colors: ['green'], season: ['autumn'], wordOfFlower: 'a steadfast heart', meanings: ['steadfastness', 'fidelity'], priceLevel: 'low', mood: ['fresh', 'lush', 'calm'], role: 'foliage' }, + { id: 86, name: 'Olive', colors: ['green', 'silver'], season: ['autumn'], wordOfFlower: 'peace', meanings: ['peace', 'victory'], priceLevel: 'low', mood: ['fresh', 'calm', 'evergreen'], role: 'foliage' }, + { id: 87, name: 'Mock Orange (Pittosporum tobira)', colors: ['white', 'green'], season: ['spring'], wordOfFlower: 'embrace', meanings: ['embrace', 'steadfast attention'], priceLevel: 'low', mood: ['fragrant', 'evergreen', 'fresh'], role: 'foliage' }, + + // ── Flowering branches ────────────────────────────────────── + { id: 88, name: 'Cherry Blossom', colors: ['pink', 'white'], season: ['spring'], wordOfFlower: 'spiritual beauty', meanings: ['spiritual beauty', 'grace'], priceLevel: 'medium', mood: ['soft', 'romantic', 'delicate'], role: 'line' }, + { id: 89, name: 'Forsythia', colors: ['yellow'], season: ['spring'], wordOfFlower: 'hope', meanings: ['hope', 'anticipation'], priceLevel: 'low', mood: ['bright', 'cheerful', 'fresh'], role: 'line' }, + { id: 90, name: 'Plum Blossom (Prunus mume)', colors: ['white', 'pink'], season: ['winter', 'spring'], wordOfFlower: 'pure heart', meanings: ['pure heart', 'innocence'], priceLevel: 'medium', mood: ['fragrant', 'elegant', 'delicate'], role: 'line' }, + { id: 91, name: 'Magnolia', colors: ['white', 'pink'], season: ['spring'], wordOfFlower: 'love of nature', meanings: ['love of nature', 'nobility'], priceLevel: 'medium', mood: ['elegant', 'soft', 'fragrant'], role: 'line' }, + { id: 92, name: 'Redbud (Cercis chinensis)', colors: ['purple', 'pink'], season: ['spring'], wordOfFlower: 'friendship', meanings: ['friendship', 'awakening'], priceLevel: 'low', mood: ['cheerful', 'soft', 'fresh'], role: 'line' }, + { id: 93, name: 'Flowering Quince (Chaenomeles)', colors: ['red', 'pink', 'orange'], season: ['spring'], wordOfFlower: 'trust', meanings: ['trust', 'shyness'], priceLevel: 'low', mood: ['cheerful', 'warm', 'rustic'], role: 'line' } ]; +/** + * @param {string} color + * @param {string[]} palette + */ +function colorMatchesPalette(color, palette) { + const terms = [color, ...(NEUTRAL_COLOR_ALIASES[color] ?? [])]; + return palette.some((p) => terms.some((term) => p.includes(term) || term.includes(p))); +} + +/** + * Flowers with silver/beige/brown lean on mood tags when palettes name hue, not tone. + * @param {FlowerRecord} flower + */ +function moodMatchWeight(flower) { + return flower.colors.some((color) => NEUTRAL_COLORS.has(color)) ? 3 : 2; +} + +/** + * @typedef {Object} FlowerCandidate + * @property {string} name + * @property {'main' | 'filler' | 'line' | 'foliage'} role + * @property {'low' | 'medium' | 'high'} priceLevel + * @property {'common' | 'limited' | 'rare'} cutAvailability + * @property {string} wordOfFlower + * @property {string[]} colors + * @property {string[]} mood + * @property {string} [family] + * @property {number} matchScore + */ + +/** + * @typedef {Object} MoodFlowerCandidates + * @property {string[]} colors + * @property {FlowerCandidate[]} main + * @property {FlowerCandidate[]} filler + * @property {FlowerCandidate[]} line + * @property {FlowerCandidate[]} foliage + */ + +/** + * @param {FlowerRecord} flower + * @param {number} score + * @returns {FlowerCandidate} + */ +function toCandidate(flower, score) { + return { + name: flower.name, + role: flower.role, + priceLevel: flower.priceLevel, + cutAvailability: flower.cutAvailability ?? 'common', + wordOfFlower: flower.wordOfFlower, + colors: flower.colors, + mood: flower.mood, + ...(flower.family ? { family: flower.family } : {}), + matchScore: score + }; +} + /** * @param {MoodAnalysis} mood * @param {string} [season] + * @returns {MoodFlowerCandidates} */ export function matchFlowersFromMood(mood, season) { const palette = mood.colorPalette.map((c) => c.toLowerCase()); @@ -110,14 +215,15 @@ export function matchFlowersFromMood(mood, season) { let score = 0; for (const color of flower.colors) { - if (palette.some((p) => p.includes(color) || color.includes(p))) { + if (colorMatchesPalette(color, palette)) { score += 2; } } + const moodWeight = moodMatchWeight(flower); for (const tag of flower.mood) { if (keywords.some((k) => k.includes(tag) || tag.includes(k))) { - score += 2; + score += moodWeight; } } @@ -125,6 +231,8 @@ export function matchFlowersFromMood(mood, season) { score += 1; } + score -= CUT_AVAILABILITY_PENALTY[flower.cutAvailability ?? 'common']; + return score; }; @@ -132,14 +240,48 @@ export function matchFlowersFromMood(mood, season) { .map((flower) => ({ flower, score: scoreFlower(flower) })) .sort((a, b) => b.score - a.score); - const mains = ranked.filter(({ flower }) => flower.role === 'main').slice(0, 2); - const subs = ranked.filter(({ flower }) => flower.role === 'sub').slice(0, 2); - const greenery = ranked.filter(({ flower }) => flower.role === 'greenery').slice(0, 1); + /** + * @param {typeof ranked} list + * @param {(flower: FlowerRecord) => boolean} roleFilter + * @param {number} limit + */ + const pickUnique = (list, roleFilter, limit) => { + /** @type {typeof ranked} */ + const picked = []; + /** @type {Set} */ + const usedFamilies = new Set(); + + for (const item of list) { + if (!roleFilter(item.flower)) continue; + + const family = item.flower.family; + if (family && usedFamilies.has(family)) continue; + + picked.push(item); + if (family) usedFamilies.add(family); + if (picked.length >= limit) break; + } + + return picked; + }; + + const mains = pickUnique(ranked, (flower) => flower.role === 'main', 4); + const accents = pickUnique( + ranked, + (flower) => flower.role === 'filler' || flower.role === 'line', + 5 + ); + const foliage = pickUnique(ranked, (flower) => flower.role === 'foliage', 2); return { - mainFlowers: mains.map(({ flower }) => flower.name), - subFlowers: subs.map(({ flower }) => flower.name), - greenery: greenery.map(({ flower }) => flower.name), - colors: mood.colorPalette.slice(0, 3) + colors: mood.colorPalette.slice(0, 3), + main: mains.map(({ flower, score }) => toCandidate(flower, score)), + filler: accents + .filter(({ flower }) => flower.role === 'filler') + .map(({ flower, score }) => toCandidate(flower, score)), + line: accents + .filter(({ flower }) => flower.role === 'line') + .map(({ flower, score }) => toCandidate(flower, score)), + foliage: foliage.map(({ flower, score }) => toCandidate(flower, score)) }; } diff --git a/src/lib/server/gemini/text.js b/src/lib/server/gemini/text.js index 508fc54..ae30d54 100644 --- a/src/lib/server/gemini/text.js +++ b/src/lib/server/gemini/text.js @@ -23,11 +23,17 @@ export async function buildBouquetRecipe(mood, userInput = {}) { const model = getTextModel(); const prompt = `You are a professional florist assistant. -Create a realistic bouquet recipe using ONLY real flowers from this candidate list. +Create a realistic Korean florist bouquet recipe using ONLY the pre-scored candidate flowers below. -Candidate mapping: +Candidate flowers (grouped by floral role; higher matchScore = stronger mood fit): ${JSON.stringify(mapped, null, 2)} +Role guide: +- main: focal blooms — choose 1-2 for mainFlowers +- filler: volume and softness — choose 1-2 for subFlowers +- line: height, rhythm, branches — choose 0-1 for subFlowers +- foliage: greenery and texture — choose 1-2 for greenery + Mood analysis: ${JSON.stringify(mood, null, 2)} @@ -47,7 +53,14 @@ Return JSON only: } Rules: -- Do not invent fantasy flowers. +- Use ONLY exact candidate names from the lists above. Do not invent, rename, or substitute flowers. +- mainFlowers must come from candidates.main only (1-2 items). +- subFlowers must combine candidates.filler and/or candidates.line only (2-4 items total). +- greenery must come from candidates.foliage only (1-2 items). +- Prefer cutAvailability "common" for mainFlowers. Use "limited" as accent at most. Avoid "rare" unless no common main fits the mood. +- Respect priceLevel against budget: favor low/medium for tighter budgets; high-price blooms sparingly. +- Do not place two flowers with the same family field in one recipe. +- Draw palette from candidate colors and mood analysis; suggested colors: ${mapped.colors.join(', ')}. - Keep the bouquet orderable from a real florist in Korea. - Budget should be ${budget}.`; @@ -71,7 +84,8 @@ Use this recipe: ${JSON.stringify(recipe, null, 2)} Rules: -- Real flowers only +- Real flowers only — use the exact flower names from the recipe +- mainFlowers are the focal blooms; subFlowers add volume and line; greenery frames the bouquet - No fantasy colors or surreal shapes - White background, soft natural lighting - Korean florist style @@ -99,6 +113,7 @@ Use this bouquet recipe: ${JSON.stringify(recipe, null, 2)} Tone: warm, professional, specific. +Mention why the main, accent, and greenery choices work together as one cohesive bouquet. Return plain text only.`; const result = await model.generateContent(prompt);