Vishnu-bot/silver-ornaments-agent
๐ช Silver Ornament Shop โ Festival Marketing Bot
Automatically generates premium, catalogue-quality Instagram campaigns for an Indian silver ornament shop. Detects upcoming festivals and generates one independent campaign per festival, each with:
- โ
Fully AI-generated creative, no real product photo used โ img2img was tried extensively but is conclusively unavailable on HF's free Inference Providers for every model tested (see
content_generator.py'sHF_IMG2IMG_MODELScomment for the investigation), and no GPU is available for a localdiffuserspipeline. The AI now generates the jewellery itself as part of one coherent text-to-image scene. - โ
Festival-aware jewelry type selection: the LLM picks which TYPE of silver jewellery suits each festival's tradition (jhumka earrings, necklace, bangles, pendant, ring, anklet, bracelet, a gemstone-inlaid set, a deity idol, or a rakhi-thread piece) from a curated vocabulary โ see
content_generator.JEWELRY_TYPE_OPTIONSโ varying across festivals rather than repeating the same piece every time. - โ A bounding-region layout engine with a 3-role type system (script greeting / serif headline / sans supporting text + CTA), a limited luxury colour palette, contrast-aware text colour, and an automatic overlap/fit validation pass โ nothing is ever cropped, overlapping, or illegible against the scene
- โ Left-aligned premium marketing layout: logo, greeting, headline, subheadline, icon-supported feature badges, and a prominent filled gold CTA button, all stacked in a left column with guaranteed contrast โ the AI scene prompt positions the jewellery in the right portion of frame to match. Logo is fully legible (not a subtle watermark).
- โ Final creative delivered as a 1080ร1350 px (4:5) Instagram portrait
- โ Instagram caption with storytelling, product highlights, and 8โ12 hashtags
- โ Trends collected from Google News RSS, Bing News RSS, DuckDuckGo, Reddit, and pytrends
- โ
Real scraped hashtags:
trend_analyzer.fetch_scraped_hashtags()pulls a live pool of currently-active hashtags for the festival name;ContentGenerator.select_best_hashtags()then has the LLM pick the subset most relevant to that specific caption/headline, rather than relying solely on the LLM's own static knowledge - โ
Final QA agent: before a campaign is accepted,
ContentGenerator.review_creative()sends the actual rendered image + all its text to a vision-capable model (Groq first, HF Inference Providers as fallback) for a holistic review โ visual quality, composition/branding coherence, grammar/persuasiveness, typography best practice, no hallucinations/distortions. If it finds real problems, the creative is regenerated from scratch (QA_MAX_RETRIES, default 2) before being flagged and exported anyway rather than blocking the campaign. Toggle withFINAL_QA_ENABLED(defaulttrue)
Note on the retired real-photo pipeline: an earlier revision of this project used uploaded ornament photos fromimages/, either regenerated via image-to-image or blended into an AI scene via a hybrid compositor. That code (image_composer.build_hybrid_creative,prepare_product_reference, andproduct_enhancer.py's blending helpers) is left in the codebase, unused, as a safety net in case real-photo mode becomes viable again (e.g. a working img2img provider is found, or a GPU becomes available).main.pyno longer calls any of it.
How it works
Check festivals.json
โ
Detect festivals 4โ15 days away
โ
For each festival (independently):
โโโ Collect live trends (5 sources, each independently fallible) โ trend_analyzer.py
โโโ Generate: jewelry_type / greeting / headline / subheadline /
โ badges(3) / design_choices / caption / hashtags / CTA โ Groq LLM
โโโ Generate a full AI scene prompt โ jewellery (of the chosen type)
โ and its festive luxury set, composed together for the strict
โ 3-zone layout below โ content_generator.py
โโโ Text-to-image generates the complete scene โ content_generator.py
โโโ Lay out logo + left-column text hierarchy + icon badges + CTA button via a bounding-region
โ layout engine: 3-role typography, contrast-aware colour,
โ limited luxury palette, auto-fitting type, and an automatic
โ overlap/fit validation pass โ image_composer.py
โโโ Save: caption.txt ยท creative.png (1080ร1350) ยท meta.json ยท trends.json
โ
If no festival detected โ fallback to user-supplied product keywordOn design fidelity, honestly: since no real product photo is used any more, there is no "fidelity" to preserve โ the AI invents the jewellery freely within the chosen jewelry_type. This is a deliberate product decision, not a fallback: img2img was tried exhaustively (5+ models, 2 different provider-routing strategies) and conclusively rejected by every available HF Inference Provider on this account, and no GPU is available to run a local diffusers pipeline instead. See content_generator.py's HF_IMG2IMG_MODELS comment for the full investigation trail if that ever changes and real-photo mode is worth reviving (the retired code is still there, unused โ see the note above).
Jewelry type selection: ContentGenerator.generate_content() has the LLM pick one jewelry_type per festival from a curated vocabulary (JEWELRY_TYPE_OPTIONS in content_generator.py) โ reasoning about which piece actually suits that festival's tradition (e.g. a bracelet or rakhi-adjacent piece for a sibling-gifting festival, a deity idol for a festival centred on worship of a specific god), and instructed to vary its choice across festivals rather than defaulting to the same type every time.
No-real-photo, always-on: every campaign now goes through the same single AI-generation path โ there's no per-campaign fallback branching to document, since there's only one path. If the AI scene generation fails entirely (all HF text-to-image models down), the campaign's creative is simply absent for that run rather than shipping a broken placeholder โ logged clearly, and the caption/hashtags/meta.json are still saved.
Setup (local)
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# Fill in GROQ_API_KEY and HF_TOKEN in .envAdd your assets:
images/
โโโ ornament_01.jpg โ used for festival 1, then deleted
โโโ ornament_02.jpg โ used for festival 2, then deleted
โโโ ...
logo/
โโโ logo.png โ composited on every creative, never deletedThen run:
python main.py # auto-detect festivals
python main.py --input "Silver Bangle" # fallback product campaign
python main.py --dry-run # show what would fire, no generationDeploying on Hugging Face Spaces (Free CPU)
- Create a new Space โ SDK = Gradio.
- Upload all project files. The YAML block above configures the Space automatically.
- In Settings โ Variables and secrets, add:
GROQ_API_KEYโ console.groq.com/keys (free)HF_TOKENโ huggingface.co/settings/tokens (Read scope)HF_IMG2IMG_ENABLED(optional, defaulttrue) โ set tofalseto skip the image-to-image chain entirely and go straight to the hybrid fallback. Useful if your HF account's providers don't currently serve img2img for any of the models inHF_IMG2IMG_MODELS(contentgenerator.py) โ check the campaign log for `"rejected the image-to-image task"` messages to confirm before disabling. See that file's comment above `HFIMG2IMG_MODELS` for how to find a working model/provider combination.- Upload ornament images to
images/and your logo tologo/via the Files tab. - Open the Space and click Generate Campaigns.
Tip:packages.txtin this repo installsfonts-dejavu-corevia apt, which provides the fonts used for text overlays. No manual setup needed on Spaces. First run only: the background-removal model (isnet-general-use, ~170 MB) downloads on the first campaign and is cached afterwards. Expect the first campaign to take noticeably longer (~30-60s extra) than subsequent ones. If your Space has no network egress on first boot, segmentation degrades gracefully โ see "Image enhancement" below.
Festival database
Festivals are stored in festivals.json โ not hardcoded in Python. Each entry has:
Update dates at the start of each year (Hindu festival dates shift annually).
Image generation & compositing
Graceful degradation: if every HF text-to-image model fails, create_ai_image() returns None and the campaign's creative is simply absent for that run โ logged clearly โ while caption/hashtags/meta.json are still saved.
Retired, unused, kept as a safety net: product_enhancer.py's segmentation/enhancement/blending helpers and image_composer.py's prepare_product_reference() / build_hybrid_creative() / build_reference_composite() implement the earlier real-photo pipeline (img2img primary, hybrid-compositor fallback). None of it is called by main.py any more โ see the note near the top of this README for why โ but it's left in place in case real-photo mode is worth reviving later.
Trend intelligence
Five independent sources โ one failure never blocks the others:
Model selection
Text โ `qwen/qwen3.6-27b` (Groq, open-weight): All models from the original priority list are currently unavailable (deprecated or already shut down as of 2026-06-21). qwen/qwen3.6-27b is the same Qwen3 family, open-weight, live on Groq, and Groq's own documented replacement for both qwen3-32b and llama-4-scout.
Images โ full-scene text-to-image via `FLUX.1-schnell โ SDXL โ SD3-medium` (HF Inference Providers): The ONLY image path now (see the note near the top of this README on why img2img was retired) โ no real photo, no conditioning image. The LLM picks a jewelry_type for the festival, generate_full_scene_prompt() writes a prompt describing that piece and its scene together, and this text-to-image chain generates the complete photograph in one call. FLUX.1-schnell preferred first for speed/quality; SDXL and SD3-medium are automatic fallbacks. HF_IMG2IMG_MODELS and the img2img code path are still in content_generator.py, gated behind the HF_IMG2IMG_ENABLED env var, in case a working provider/model combination is found later.
Segmentation โ `isnet-general-use` (via `rembg`): No longer called by main.py (see above) โ kept in product_enhancer.py for the retired real-photo pipeline. rembg's own recommended model for general product/object cutouts (distinct from u2net_human_seg, which targets people).
Output files
Each campaign writes to output/<festival-name>_<date>/:
