Update seo-multi-channel: remove image skill dependencies
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
---
|
||||
name: seo-multi-channel
|
||||
description: Generate multi-channel marketing content (Facebook, Ads, Blog, X) with Thai language support, image generation, and website-creator integration. Use when user wants to create content for multiple channels from a single topic.
|
||||
description: Generate multi-channel marketing content (Facebook, Ads, Blog, X) with Thai language support and website-creator integration. Use when user wants to create content for multiple channels from a single topic.
|
||||
---
|
||||
|
||||
# 🎯 SEO Multi-Channel Content Generator
|
||||
|
||||
**Skill Name:** `seo-multi-channel`
|
||||
**Category:** `deep`
|
||||
**Load Skills:** `['image-generation', 'image-edit', 'website-creator']`
|
||||
**Load Skills:** `['website-creator']`
|
||||
|
||||
---
|
||||
|
||||
@@ -17,8 +17,7 @@ Generate marketing content for multiple channels from a single topic with:
|
||||
|
||||
- ✅ **Priority Channels:** Facebook > Facebook Ads > Google Ads > Blog > X (Twitter)
|
||||
- ✅ **Thai Language Support:** Full Thai text processing with PyThaiNLP
|
||||
- ✅ **Image Generation:** Auto-generate images for social/ads, save to website repo for blog
|
||||
- ✅ **Product Image Handling:** Browse website repo first, then ask user or enhance with image-edit
|
||||
- ✅ **Image Handling:** Browse website repo for product images, or ask user to provide
|
||||
- ✅ **Website-Creator Integration:** Auto-publish blog posts to Astro content collections
|
||||
- ✅ **API-Ready Output:** Structured JSON for future ad platform API integration
|
||||
- ✅ **Per-Project Context:** Context files in each website repo
|
||||
@@ -169,13 +168,11 @@ Output:
|
||||
|
||||
2. If images found:
|
||||
- Select best image (highest quality, product-focused)
|
||||
- Call image-edit skill:
|
||||
prompt: "Enhance product image for {channel}, professional lighting, clean background, {channel}-specific dimensions"
|
||||
- Ask user: "Use this image or provide a new one?"
|
||||
|
||||
3. If no images found:
|
||||
- Ask user: "No product images found in repo. Please provide image path or URL."
|
||||
- Wait for user to provide
|
||||
- Then call image-edit
|
||||
```
|
||||
|
||||
#### **Non-Product Content:**
|
||||
@@ -187,8 +184,8 @@ Output:
|
||||
- Stats → Infographic with charts
|
||||
- News → Clean, modern announcement style
|
||||
|
||||
2. Call image-generation skill:
|
||||
prompt: "{content_type} illustration for {topic}, {style}, Thai-friendly aesthetic, {channel}-optimized dimensions"
|
||||
2. Ask user to provide image:
|
||||
- "For this content, please provide an image or use: [suggestion]"
|
||||
|
||||
3. Save images:
|
||||
- Social/Ads → seo-multi-channel/generated-images/{topic}/{channel}/
|
||||
@@ -602,8 +599,8 @@ Each piece of content is scored before output:
|
||||
2. **Formality Detection:** Auto-detects from brand voice context. Defaults to casual for social, normal for blog.
|
||||
|
||||
3. **Image Handling:**
|
||||
- Product content → Browse repo first → Edit with image-edit
|
||||
- Non-product → Generate fresh with image-generation
|
||||
- Product content → Browse repo first → Ask user to confirm or provide
|
||||
- Non-product → Ask user to provide image
|
||||
- Blog images → Website repo
|
||||
- Social/Ads images → Separate folder
|
||||
|
||||
@@ -617,8 +614,6 @@ Each piece of content is scored before output:
|
||||
|
||||
## 🔄 Integration with Other Skills
|
||||
|
||||
- **image-generation:** Called for fresh images (non-product content)
|
||||
- **image-edit:** Called for product images (browse repo first)
|
||||
- **website-creator:** Blog posts published to Astro content collections
|
||||
- **seo-analyzers:** Quality scoring and Thai language analysis
|
||||
- **seo-data:** Performance data for content optimization
|
||||
@@ -630,8 +625,7 @@ Each piece of content is scored before output:
|
||||
|
||||
- ✅ Content generated for all selected channels
|
||||
- ✅ Thai language processing accurate (word count, keyword density)
|
||||
- ✅ Product images found/enhanced or user asked to provide
|
||||
- ✅ Fresh images generated for non-product content
|
||||
- ✅ Product images found or user asked to provide
|
||||
- ✅ Blog posts published to Astro (if enabled)
|
||||
- ✅ Git commit + push successful (triggers auto-deploy)
|
||||
- ✅ Output structures API-ready for future integration
|
||||
|
||||
@@ -19,12 +19,14 @@ import yaml
|
||||
|
||||
# Load environment variables
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Thai language processing
|
||||
try:
|
||||
from pythainlp import word_tokenize, sent_tokenize
|
||||
from pythainlp.util import normalize
|
||||
|
||||
THAI_SUPPORT = True
|
||||
except ImportError:
|
||||
THAI_SUPPORT = False
|
||||
@@ -48,7 +50,7 @@ class ThaiTextProcessor:
|
||||
def count_sentences(text: str) -> int:
|
||||
"""Count Thai sentences"""
|
||||
if not THAI_SUPPORT:
|
||||
return len(text.split('.'))
|
||||
return len(text.split("."))
|
||||
|
||||
sentences = sent_tokenize(text, engine="whitespace")
|
||||
return len(sentences)
|
||||
@@ -70,11 +72,11 @@ class ThaiTextProcessor:
|
||||
@staticmethod
|
||||
def detect_language(text: str) -> str:
|
||||
"""Detect if content is Thai or English"""
|
||||
thai_chars = sum(1 for c in text if '\u0E00' <= c <= '\u0E7F')
|
||||
thai_chars = sum(1 for c in text if "\u0e00" <= c <= "\u0e7f")
|
||||
total_chars = len(text)
|
||||
thai_ratio = thai_chars / total_chars if total_chars > 0 else 0
|
||||
|
||||
return 'th' if thai_ratio > 0.3 else 'en'
|
||||
return "th" if thai_ratio > 0.3 else "en"
|
||||
|
||||
|
||||
class ChannelTemplate:
|
||||
@@ -87,16 +89,16 @@ class ChannelTemplate:
|
||||
|
||||
def _load_template(self) -> Dict:
|
||||
"""Load YAML template"""
|
||||
with open(self.template_path, 'r', encoding='utf-8') as f:
|
||||
with open(self.template_path, "r", encoding="utf-8") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def get_specs(self) -> Dict:
|
||||
"""Get channel specifications"""
|
||||
return self.template.get('fields', {})
|
||||
return self.template.get("fields", {})
|
||||
|
||||
def get_quality_requirements(self) -> Dict:
|
||||
"""Get quality requirements"""
|
||||
return self.template.get('quality', {})
|
||||
return self.template.get("quality", {})
|
||||
|
||||
|
||||
class ImageHandler:
|
||||
@@ -110,52 +112,44 @@ class ImageHandler:
|
||||
"""Find existing product images in website repo"""
|
||||
import glob
|
||||
|
||||
extensions = ['.jpg', '.jpeg', '.png', '.webp']
|
||||
extensions = [".jpg", ".jpeg", ".png", ".webp"]
|
||||
found_images = []
|
||||
|
||||
search_patterns = [
|
||||
f"**/*{product_name}*{{ext}}" for ext in extensions
|
||||
] + [
|
||||
search_patterns = [f"**/*{product_name}*{{ext}}" for ext in extensions] + [
|
||||
"public/images/**/*{ext}",
|
||||
"src/assets/**/*{ext}"
|
||||
"src/assets/**/*{ext}",
|
||||
]
|
||||
|
||||
for pattern in search_patterns:
|
||||
matches = glob.glob(
|
||||
os.path.join(website_repo, pattern.format(ext='*')),
|
||||
recursive=True
|
||||
os.path.join(website_repo, pattern.format(ext="*")), recursive=True
|
||||
)
|
||||
# Try specific extensions
|
||||
for ext in extensions:
|
||||
specific_matches = glob.glob(
|
||||
os.path.join(website_repo, pattern.format(ext=ext)),
|
||||
recursive=True
|
||||
os.path.join(website_repo, pattern.format(ext=ext)), recursive=True
|
||||
)
|
||||
found_images.extend(specific_matches)
|
||||
|
||||
return list(set(found_images))[:10]
|
||||
|
||||
def generate_image_for_channel(self, topic: str, channel: str, content_type: str) -> str:
|
||||
def generate_image_for_channel(
|
||||
self, topic: str, channel: str, content_type: str
|
||||
) -> str:
|
||||
"""
|
||||
Generate image for content.
|
||||
For product: browse repo first, then ask user or use image-edit
|
||||
For non-product: generate fresh with image-generation
|
||||
Handle image for content.
|
||||
For product: browse repo first, ask user to confirm/provide
|
||||
For non-product: ask user to provide image
|
||||
"""
|
||||
# This would call the image-generation or image-edit skills
|
||||
# For now, return placeholder
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_dir = os.path.join(
|
||||
self.output_base,
|
||||
self._slugify(topic),
|
||||
channel,
|
||||
"images"
|
||||
self.output_base, self._slugify(topic), channel, "images"
|
||||
)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
image_path = os.path.join(output_dir, f"generated_{timestamp}.png")
|
||||
|
||||
# Placeholder - in real implementation, would call image-generation skill
|
||||
print(f" [Image Generation] Would generate image for {channel}")
|
||||
print(f" [Image] Please provide image for: {channel}")
|
||||
print(f" Topic: {topic}, Type: {content_type}")
|
||||
|
||||
return image_path
|
||||
@@ -163,9 +157,10 @@ class ImageHandler:
|
||||
def _slugify(self, text: str) -> str:
|
||||
"""Convert text to URL-friendly slug"""
|
||||
import re
|
||||
slug = re.sub(r'[^\w\s-]', '', text.lower())
|
||||
slug = re.sub(r'[-\s]+', '-', slug)
|
||||
return slug.strip('-_')
|
||||
|
||||
slug = re.sub(r"[^\w\s-]", "", text.lower())
|
||||
slug = re.sub(r"[-\s]+", "-", slug)
|
||||
return slug.strip("-_")
|
||||
|
||||
|
||||
class ContentGenerator:
|
||||
@@ -177,7 +172,7 @@ class ContentGenerator:
|
||||
channels: List[str],
|
||||
website_repo: Optional[str] = None,
|
||||
auto_publish: bool = False,
|
||||
language: Optional[str] = None
|
||||
language: Optional[str] = None,
|
||||
):
|
||||
self.topic = topic
|
||||
self.channels = channels
|
||||
@@ -189,34 +184,35 @@ class ContentGenerator:
|
||||
|
||||
# Initialize components
|
||||
self.text_processor = ThaiTextProcessor()
|
||||
self.image_handler = ImageHandler(os.getenv("CHUTES_API_TOKEN", ""))
|
||||
|
||||
# Load templates
|
||||
self.templates = {}
|
||||
for channel in channels:
|
||||
template_name = self._get_template_name(channel)
|
||||
if template_name:
|
||||
self.templates[channel] = ChannelTemplate(template_name, self.templates_dir)
|
||||
self.templates[channel] = ChannelTemplate(
|
||||
template_name, self.templates_dir
|
||||
)
|
||||
|
||||
def _get_template_name(self, channel: str) -> Optional[str]:
|
||||
"""Map channel name to template file"""
|
||||
mapping = {
|
||||
'facebook': 'facebook',
|
||||
'facebook_ads': 'facebook_ads',
|
||||
'google_ads': 'google_ads',
|
||||
'blog': 'blog',
|
||||
'x': 'x_thread',
|
||||
'twitter': 'x_thread'
|
||||
"facebook": "facebook",
|
||||
"facebook_ads": "facebook_ads",
|
||||
"google_ads": "google_ads",
|
||||
"blog": "blog",
|
||||
"x": "x_thread",
|
||||
"twitter": "x_thread",
|
||||
}
|
||||
return mapping.get(channel.lower())
|
||||
|
||||
def generate_all(self) -> Dict[str, Any]:
|
||||
"""Generate content for all channels"""
|
||||
results = {
|
||||
'topic': self.topic,
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'channels': {},
|
||||
'summary': {}
|
||||
"topic": self.topic,
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"channels": {},
|
||||
"summary": {},
|
||||
}
|
||||
|
||||
print(f"\n🎯 Generating content for: {self.topic}")
|
||||
@@ -227,7 +223,7 @@ class ContentGenerator:
|
||||
if channel in self.templates:
|
||||
print(f" Generating {channel}...")
|
||||
channel_result = self._generate_for_channel(channel)
|
||||
results['channels'][channel] = channel_result
|
||||
results["channels"][channel] = channel_result
|
||||
|
||||
# Save results
|
||||
self._save_results(results)
|
||||
@@ -244,101 +240,106 @@ class ContentGenerator:
|
||||
|
||||
# Generate variations (placeholder - real implementation would use LLM)
|
||||
variations = []
|
||||
num_variations = template.template.get('output', {}).get('variations', 5)
|
||||
num_variations = template.template.get("output", {}).get("variations", 5)
|
||||
|
||||
for i in range(num_variations):
|
||||
variation = self._create_variation(channel, i, lang, specs)
|
||||
variations.append(variation)
|
||||
|
||||
return {
|
||||
'channel': channel,
|
||||
'language': lang,
|
||||
'variations': variations,
|
||||
'api_ready': template.template.get('api_ready', False)
|
||||
"channel": channel,
|
||||
"language": lang,
|
||||
"variations": variations,
|
||||
"api_ready": template.template.get("api_ready", False),
|
||||
}
|
||||
|
||||
def _create_variation(
|
||||
self,
|
||||
channel: str,
|
||||
variation_num: int,
|
||||
language: str,
|
||||
specs: Dict
|
||||
self, channel: str, variation_num: int, language: str, specs: Dict
|
||||
) -> Dict:
|
||||
"""Create single content variation"""
|
||||
# This is a placeholder - real implementation would call LLM
|
||||
# with proper prompts based on channel template
|
||||
|
||||
base_variation = {
|
||||
'id': f"{channel}_var_{variation_num + 1}",
|
||||
'created_at': datetime.now().isoformat()
|
||||
"id": f"{channel}_var_{variation_num + 1}",
|
||||
"created_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
# Channel-specific structure
|
||||
if channel == 'facebook':
|
||||
base_variation.update({
|
||||
'primary_text': f"[Facebook Post {variation_num + 1}] {self.topic}...",
|
||||
'headline': f"[Headline] {self.topic}",
|
||||
'cta': "เรียนรู้เพิ่มเติม" if language == 'th' else "Learn More",
|
||||
'hashtags': [f"#{self.topic.replace(' ', '')}"],
|
||||
'image': {
|
||||
'path': self.image_handler.generate_image_for_channel(
|
||||
self.topic, channel, 'social'
|
||||
if channel == "facebook":
|
||||
base_variation.update(
|
||||
{
|
||||
"primary_text": f"[Facebook Post {variation_num + 1}] {self.topic}...",
|
||||
"headline": f"[Headline] {self.topic}",
|
||||
"cta": "เรียนรู้เพิ่มเติม" if language == "th" else "Learn More",
|
||||
"hashtags": [f"#{self.topic.replace(' ', '')}"],
|
||||
"image": {
|
||||
"path": self.generate_image_for_channel(
|
||||
self.topic, channel, "social"
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
elif channel == 'facebook_ads':
|
||||
base_variation.update({
|
||||
'primary_text': f"[FB Ad Primary Text] {self.topic}...",
|
||||
'headline': f"[FB Ad Headline - 40 chars]",
|
||||
'description': f"[FB Ad Description - 90 chars]",
|
||||
'cta': "SHOP_NOW",
|
||||
'api_ready': {
|
||||
'platform': 'meta',
|
||||
'api_version': 'v18.0',
|
||||
'endpoint': '/act_{ad_account_id}/adcreatives'
|
||||
elif channel == "facebook_ads":
|
||||
base_variation.update(
|
||||
{
|
||||
"primary_text": f"[FB Ad Primary Text] {self.topic}...",
|
||||
"headline": f"[FB Ad Headline - 40 chars]",
|
||||
"description": f"[FB Ad Description - 90 chars]",
|
||||
"cta": "SHOP_NOW",
|
||||
"api_ready": {
|
||||
"platform": "meta",
|
||||
"api_version": "v18.0",
|
||||
"endpoint": "/act_{ad_account_id}/adcreatives",
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
elif channel == 'google_ads':
|
||||
base_variation.update({
|
||||
'headlines': [
|
||||
{'text': f"[Headline {i+1}] {self.topic}"}
|
||||
for i in range(15)
|
||||
elif channel == "google_ads":
|
||||
base_variation.update(
|
||||
{
|
||||
"headlines": [
|
||||
{"text": f"[Headline {i + 1}] {self.topic}"} for i in range(15)
|
||||
],
|
||||
'descriptions': [
|
||||
{'text': f"[Description {i+1}] Learn more about {self.topic}"}
|
||||
"descriptions": [
|
||||
{"text": f"[Description {i + 1}] Learn more about {self.topic}"}
|
||||
for i in range(4)
|
||||
],
|
||||
'keywords': [self.topic, f"บริการ {self.topic}"],
|
||||
'api_ready': {
|
||||
'platform': 'google',
|
||||
'api_version': 'v15.0',
|
||||
'endpoint': '/google.ads.googleads.v15.services/GoogleAdsService:Mutate'
|
||||
}
|
||||
})
|
||||
|
||||
elif channel == 'blog':
|
||||
base_variation.update({
|
||||
'markdown': self._generate_blog_markdown(language),
|
||||
'frontmatter': {
|
||||
'title': f"{self.topic} - Complete Guide",
|
||||
'description': f"Learn about {self.topic}",
|
||||
'slug': self._slugify(self.topic),
|
||||
'lang': language
|
||||
"keywords": [self.topic, f"บริการ {self.topic}"],
|
||||
"api_ready": {
|
||||
"platform": "google",
|
||||
"api_version": "v15.0",
|
||||
"endpoint": "/google.ads.googleads.v15.services/GoogleAdsService:Mutate",
|
||||
},
|
||||
'word_count': 2000 if language == 'en' else 1500,
|
||||
'publish_status': 'draft'
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
elif channel in ['x', 'twitter']:
|
||||
base_variation.update({
|
||||
'tweets': [
|
||||
elif channel == "blog":
|
||||
base_variation.update(
|
||||
{
|
||||
"markdown": self._generate_blog_markdown(language),
|
||||
"frontmatter": {
|
||||
"title": f"{self.topic} - Complete Guide",
|
||||
"description": f"Learn about {self.topic}",
|
||||
"slug": self._slugify(self.topic),
|
||||
"lang": language,
|
||||
},
|
||||
"word_count": 2000 if language == "en" else 1500,
|
||||
"publish_status": "draft",
|
||||
}
|
||||
)
|
||||
|
||||
elif channel in ["x", "twitter"]:
|
||||
base_variation.update(
|
||||
{
|
||||
"tweets": [
|
||||
f"[Tweet {i + 1}/7] Content about {self.topic}..."
|
||||
for i in range(7)
|
||||
],
|
||||
'thread_title': f"Everything about {self.topic} 🧵"
|
||||
})
|
||||
"thread_title": f"Everything about {self.topic} 🧵",
|
||||
}
|
||||
)
|
||||
|
||||
return base_variation
|
||||
|
||||
@@ -354,7 +355,7 @@ slug: {slug}
|
||||
lang: {language}
|
||||
category: guides
|
||||
tags: ["{self.topic}", "guide"]
|
||||
created: {datetime.now().strftime('%Y-%m-%d')}
|
||||
created: {datetime.now().strftime("%Y-%m-%d")}
|
||||
---
|
||||
|
||||
# {self.topic}: Complete Guide
|
||||
@@ -387,14 +388,11 @@ created: {datetime.now().strftime('%Y-%m-%d')}
|
||||
|
||||
def _save_results(self, results: Dict):
|
||||
"""Save results to output directory"""
|
||||
output_dir = os.path.join(
|
||||
self.output_base,
|
||||
self._slugify(self.topic)
|
||||
)
|
||||
output_dir = os.path.join(self.output_base, self._slugify(self.topic))
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
output_file = os.path.join(output_dir, "results.json")
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
json.dump(results, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"\n✅ Results saved to: {output_file}")
|
||||
@@ -402,51 +400,50 @@ created: {datetime.now().strftime('%Y-%m-%d')}
|
||||
def _slugify(self, text: str) -> str:
|
||||
"""Convert text to URL-friendly slug"""
|
||||
import re
|
||||
slug = re.sub(r'[^\w\s-]', '', text.lower())
|
||||
slug = re.sub(r'[-\s]+', '-', slug)
|
||||
return slug.strip('-_')
|
||||
|
||||
slug = re.sub(r"[^\w\s-]", "", text.lower())
|
||||
slug = re.sub(r"[-\s]+", "-", slug)
|
||||
return slug.strip("-_")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate multi-channel marketing content from a single topic'
|
||||
description="Generate multi-channel marketing content from a single topic"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--topic', '-t',
|
||||
required=True,
|
||||
help='Topic to generate content about'
|
||||
"--topic", "-t", required=True, help="Topic to generate content about"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--channels', '-c',
|
||||
nargs='+',
|
||||
default=['facebook', 'facebook_ads', 'google_ads', 'blog', 'x'],
|
||||
choices=['facebook', 'facebook_ads', 'google_ads', 'blog', 'x', 'twitter'],
|
||||
help='Channels to generate content for'
|
||||
"--channels",
|
||||
"-c",
|
||||
nargs="+",
|
||||
default=["facebook", "facebook_ads", "google_ads", "blog", "x"],
|
||||
choices=["facebook", "facebook_ads", "google_ads", "blog", "x", "twitter"],
|
||||
help="Channels to generate content for",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--website-repo', '-w',
|
||||
help='Path to website repository (for blog auto-publish)'
|
||||
"--website-repo",
|
||||
"-w",
|
||||
help="Path to website repository (for blog auto-publish)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--auto-publish',
|
||||
action='store_true',
|
||||
help='Auto-publish blog posts to website'
|
||||
"--auto-publish", action="store_true", help="Auto-publish blog posts to website"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--language', '-l',
|
||||
choices=['th', 'en'],
|
||||
help='Content language (default: auto-detect)'
|
||||
"--language",
|
||||
"-l",
|
||||
choices=["th", "en"],
|
||||
help="Content language (default: auto-detect)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--product-name', '-p',
|
||||
help='Product name (for product image handling)'
|
||||
"--product-name", "-p", help="Product name (for product image handling)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
@@ -457,7 +454,7 @@ def main():
|
||||
channels=args.channels,
|
||||
website_repo=args.website_repo,
|
||||
auto_publish=args.auto_publish,
|
||||
language=args.language
|
||||
language=args.language,
|
||||
)
|
||||
|
||||
# Generate content
|
||||
@@ -468,11 +465,11 @@ def main():
|
||||
print(f" Topic: {results['topic']}")
|
||||
print(f" Channels generated: {len(results['channels'])}")
|
||||
|
||||
for channel, data in results['channels'].items():
|
||||
for channel, data in results["channels"].items():
|
||||
print(f" - {channel}: {len(data['variations'])} variations")
|
||||
|
||||
print(f"\n✨ Done!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -2,161 +2,20 @@
|
||||
"""
|
||||
Image Integration Module
|
||||
|
||||
Integrates with image-generation and image-edit skills.
|
||||
Handles product vs non-product image workflows.
|
||||
Since image-generation and image-edit skills are removed, this module
|
||||
provides utilities to find existing images and ask user to provide new ones.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import glob
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class ImageIntegration:
|
||||
"""Integrate with image-generation and image-edit skills"""
|
||||
|
||||
def __init__(self, skills_base_path: str = None):
|
||||
"""
|
||||
Initialize image integration
|
||||
|
||||
Args:
|
||||
skills_base_path: Base path to skills directory
|
||||
"""
|
||||
if skills_base_path is None:
|
||||
# Default: assume we're in skills/seo-multi-channel/scripts/
|
||||
base = Path(__file__).parent.parent.parent
|
||||
self.skills_base = str(base)
|
||||
else:
|
||||
self.skills_base = skills_base
|
||||
|
||||
self.image_gen_script = os.path.join(self.skills_base, 'image-generation/scripts/image_gen.py')
|
||||
self.image_edit_script = os.path.join(self.skills_base, 'image-edit/scripts/image_edit.py')
|
||||
|
||||
def generate_image(self, prompt: str, output_dir: str, width: int = 1024,
|
||||
height: int = 1024, topic: str = None, channel: str = None) -> str:
|
||||
"""
|
||||
Generate image using image-generation skill
|
||||
|
||||
Args:
|
||||
prompt: Image generation prompt
|
||||
output_dir: Directory to save image
|
||||
width: Image width
|
||||
height: Image height
|
||||
topic: Topic name (for filename)
|
||||
channel: Channel name (for subfolder)
|
||||
|
||||
Returns:
|
||||
Path to generated image
|
||||
"""
|
||||
# Create output directory
|
||||
if topic and channel:
|
||||
output_path = os.path.join(output_dir, topic, channel, 'images')
|
||||
else:
|
||||
output_path = output_dir
|
||||
|
||||
os.makedirs(output_path, exist_ok=True)
|
||||
|
||||
# Build command
|
||||
cmd = [
|
||||
sys.executable,
|
||||
self.image_gen_script,
|
||||
'generate',
|
||||
prompt,
|
||||
'--width', str(width),
|
||||
'--height', str(height)
|
||||
]
|
||||
|
||||
print(f"\n🎨 Generating image...")
|
||||
print(f" Prompt: {prompt[:100]}...")
|
||||
print(f" Size: {width}x{height}")
|
||||
|
||||
try:
|
||||
# Run image generation
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, cwd=os.path.dirname(self.image_gen_script))
|
||||
|
||||
if result.returncode == 0:
|
||||
# Parse output (format: "filename.png [id]")
|
||||
output_line = result.stdout.strip().split('\n')[-1]
|
||||
image_path = output_line.split(' ')[0]
|
||||
|
||||
# Move to our output directory if needed
|
||||
if image_path and os.path.exists(image_path):
|
||||
dest_path = os.path.join(output_path, os.path.basename(image_path))
|
||||
if image_path != dest_path:
|
||||
import shutil
|
||||
shutil.copy(image_path, dest_path)
|
||||
print(f" ✓ Saved: {dest_path}")
|
||||
return dest_path
|
||||
|
||||
print(f" ✗ Generation failed: {result.stderr}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ Error: {e}")
|
||||
return None
|
||||
|
||||
def edit_product_image(self, base_image_path: str, edit_prompt: str,
|
||||
output_dir: str, topic: str = None, channel: str = None) -> str:
|
||||
"""
|
||||
Edit product image using image-edit skill
|
||||
|
||||
Args:
|
||||
base_image_path: Path to existing product image
|
||||
edit_prompt: Edit instructions
|
||||
output_dir: Directory to save edited image
|
||||
topic: Topic name
|
||||
channel: Channel name
|
||||
|
||||
Returns:
|
||||
Path to edited image
|
||||
"""
|
||||
if not os.path.exists(base_image_path):
|
||||
print(f" ✗ Base image not found: {base_image_path}")
|
||||
return None
|
||||
|
||||
# Create output directory
|
||||
if topic and channel:
|
||||
output_path = os.path.join(output_dir, topic, channel, 'images')
|
||||
else:
|
||||
output_path = output_dir
|
||||
|
||||
os.makedirs(output_path, exist_ok=True)
|
||||
|
||||
# Build command
|
||||
cmd = [
|
||||
sys.executable,
|
||||
self.image_edit_script,
|
||||
edit_prompt,
|
||||
base_image_path
|
||||
]
|
||||
|
||||
print(f"\n✏️ Editing product image...")
|
||||
print(f" Base: {base_image_path}")
|
||||
print(f" Edit: {edit_prompt[:100]}...")
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, cwd=os.path.dirname(self.image_edit_script))
|
||||
|
||||
if result.returncode == 0:
|
||||
output_line = result.stdout.strip().split('\n')[-1]
|
||||
image_path = output_line.split(' ')[0]
|
||||
|
||||
if image_path and os.path.exists(image_path):
|
||||
dest_path = os.path.join(output_path, os.path.basename(image_path))
|
||||
if image_path != dest_path:
|
||||
import shutil
|
||||
shutil.copy(image_path, dest_path)
|
||||
print(f" ✓ Saved: {dest_path}")
|
||||
return dest_path
|
||||
|
||||
print(f" ✗ Edit failed: {result.stderr}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ Error: {e}")
|
||||
return None
|
||||
def __init__(self, skills_base_path: str = ""):
|
||||
pass
|
||||
|
||||
def find_product_images(self, product_name: str, website_repo: str) -> List[str]:
|
||||
"""
|
||||
@@ -169,136 +28,82 @@ class ImageIntegration:
|
||||
Returns:
|
||||
List of image paths
|
||||
"""
|
||||
import glob
|
||||
if not website_repo or not os.path.exists(website_repo):
|
||||
return []
|
||||
|
||||
extensions = ['.jpg', '.jpeg', '.png', '.webp']
|
||||
extensions = [".jpg", ".jpeg", ".png", ".webp"]
|
||||
found_images = []
|
||||
|
||||
# Search patterns
|
||||
patterns = [
|
||||
f"**/*{product_name}*{{ext}}",
|
||||
f"public/images/**/*{{ext}}",
|
||||
f"src/assets/**/*{{ext}}"
|
||||
f"src/assets/**/*{{ext}}",
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
for ext in extensions:
|
||||
search_pattern = pattern.format(ext=ext)
|
||||
matches = glob.glob(os.path.join(website_repo, search_pattern), recursive=True)
|
||||
found_images.extend(matches[:5]) # Limit per pattern
|
||||
matches = glob.glob(
|
||||
os.path.join(website_repo, search_pattern), recursive=True
|
||||
)
|
||||
found_images.extend(matches[:5])
|
||||
|
||||
return list(set(found_images))[:10] # Return unique, max 10
|
||||
return list(set(found_images))[:10]
|
||||
|
||||
def handle_product_content(self, product_name: str, website_repo: str,
|
||||
edit_prompt: str, output_dir: str,
|
||||
topic: str, channel: str) -> Optional[str]:
|
||||
"""
|
||||
Handle image for product content
|
||||
|
||||
Workflow:
|
||||
1. Browse website repo for product images
|
||||
2. If found: edit with image-edit
|
||||
3. If not found: ask user to provide
|
||||
|
||||
Args:
|
||||
product_name: Product name
|
||||
website_repo: Path to website repo
|
||||
edit_prompt: Edit instructions
|
||||
output_dir: Output directory
|
||||
topic: Topic name
|
||||
channel: Channel name
|
||||
|
||||
Returns:
|
||||
Path to image or None
|
||||
"""
|
||||
def handle_product_content(
|
||||
self, product_name: str, website_repo: str
|
||||
) -> Optional[List[str]]:
|
||||
"""Handle image for product content - returns found images for user to select"""
|
||||
print(f"\n🔍 Looking for product images: {product_name}")
|
||||
|
||||
# Step 1: Find existing images
|
||||
images = self.find_product_images(product_name, website_repo)
|
||||
|
||||
if images:
|
||||
print(f" ✓ Found {len(images)} image(s)")
|
||||
best_image = images[0] # Use first/best match
|
||||
|
||||
# Step 2: Edit image
|
||||
return self.edit_product_image(
|
||||
best_image,
|
||||
edit_prompt,
|
||||
output_dir,
|
||||
topic,
|
||||
channel
|
||||
)
|
||||
print(f" ✓ Found {len(images)} image(s):")
|
||||
for i, img in enumerate(images[:5], 1):
|
||||
print(f" {i}. {img}")
|
||||
return images
|
||||
else:
|
||||
print(f" ✗ No product images found in repo")
|
||||
print(f" Please provide product image manually")
|
||||
return None
|
||||
|
||||
def handle_non_product_content(self, content_type: str, topic: str,
|
||||
output_dir: str, channel: str) -> Optional[str]:
|
||||
def suggest_non_product_image(self, content_type: str, topic: str) -> str:
|
||||
"""
|
||||
Generate fresh image for non-product content
|
||||
Suggest image for non-product content
|
||||
|
||||
Args:
|
||||
content_type: Type (service, stats, knowledge)
|
||||
topic: Topic name
|
||||
output_dir: Output directory
|
||||
channel: Channel name
|
||||
|
||||
Returns:
|
||||
Path to generated image
|
||||
Suggestion message
|
||||
"""
|
||||
# Create prompt based on content type
|
||||
prompts = {
|
||||
'service': f"Professional illustration of {topic}, modern flat design, business context, Thai-friendly aesthetic",
|
||||
'stats': f"Data visualization infographic for {topic}, clean charts, professional style",
|
||||
'knowledge': f"Educational illustration for {topic}, clear visual metaphor, engaging style",
|
||||
'default': f"Professional image for {topic}, modern design, high quality"
|
||||
suggestions = {
|
||||
"service": f"Please provide a professional service illustration for: {topic}",
|
||||
"stats": f"Please provide a data visualization/infographic image for: {topic}",
|
||||
"knowledge": f"Please provide an educational illustration for: {topic}",
|
||||
"default": f"Please provide an image for: {topic}",
|
||||
}
|
||||
|
||||
prompt = prompts.get(content_type, prompts['default'])
|
||||
|
||||
# Generate image
|
||||
return self.generate_image(
|
||||
prompt,
|
||||
output_dir,
|
||||
topic=topic,
|
||||
channel=channel
|
||||
)
|
||||
return suggestions.get(content_type, suggestions["default"])
|
||||
|
||||
|
||||
def main():
|
||||
"""Test image integration"""
|
||||
parser = argparse.ArgumentParser(description='Test Image Integration')
|
||||
parser.add_argument('--action', choices=['generate', 'edit', 'find'], required=True)
|
||||
parser.add_argument('--prompt', help='Image prompt or edit instructions')
|
||||
parser.add_argument('--topic', help='Topic name')
|
||||
parser.add_argument('--channel', help='Channel name')
|
||||
parser.add_argument('--output-dir', default='./output', help='Output directory')
|
||||
parser.add_argument('--product-name', help='Product name (for find action)')
|
||||
parser.add_argument('--website-repo', help='Website repo path (for find action)')
|
||||
parser = argparse.ArgumentParser(description="Test Image Integration")
|
||||
parser.add_argument("--action", choices=["find", "suggest"], required=True)
|
||||
parser.add_argument("--topic", help="Topic name")
|
||||
parser.add_argument("--product-name", help="Product name (for find action)")
|
||||
parser.add_argument("--website-repo", help="Website repo path (for find action)")
|
||||
parser.add_argument(
|
||||
"--content-type", default="default", help="Content type (for suggest action)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
integration = ImageIntegration()
|
||||
|
||||
if args.action == 'generate':
|
||||
result = integration.handle_non_product_content(
|
||||
'service', args.topic, args.output_dir, args.channel
|
||||
)
|
||||
print(f"\nResult: {result}")
|
||||
|
||||
elif args.action == 'edit':
|
||||
if not args.product_name or not args.website_repo:
|
||||
print("Error: --product-name and --website-repo required for edit")
|
||||
return
|
||||
|
||||
result = integration.handle_product_content(
|
||||
args.product_name, args.website_repo, args.prompt,
|
||||
args.output_dir, args.topic, args.channel
|
||||
)
|
||||
print(f"\nResult: {result}")
|
||||
|
||||
elif args.action == 'find':
|
||||
if args.action == "find":
|
||||
if not args.product_name or not args.website_repo:
|
||||
print("Error: --product-name and --website-repo required for find")
|
||||
return
|
||||
@@ -308,6 +113,12 @@ def main():
|
||||
for img in images:
|
||||
print(f" - {img}")
|
||||
|
||||
elif args.action == "suggest":
|
||||
suggestion = integration.suggest_non_product_image(
|
||||
args.content_type, args.topic or "your topic"
|
||||
)
|
||||
print(f"\n{suggestion}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user