Update seo-multi-channel: remove image skill dependencies

This commit is contained in:
Kunthawat Greethong
2026-03-22 13:08:30 +07:00
parent fe48c4c294
commit 48595100a1
3 changed files with 279 additions and 477 deletions

View File

@@ -1,13 +1,13 @@
--- ---
name: seo-multi-channel 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 # 🎯 SEO Multi-Channel Content Generator
**Skill Name:** `seo-multi-channel` **Skill Name:** `seo-multi-channel`
**Category:** `deep` **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) -**Priority Channels:** Facebook > Facebook Ads > Google Ads > Blog > X (Twitter)
-**Thai Language Support:** Full Thai text processing with PyThaiNLP -**Thai Language Support:** Full Thai text processing with PyThaiNLP
-**Image Generation:** Auto-generate images for social/ads, save to website repo for blog -**Image Handling:** Browse website repo for product images, or ask user to provide
-**Product Image Handling:** Browse website repo first, then ask user or enhance with image-edit
-**Website-Creator Integration:** Auto-publish blog posts to Astro content collections -**Website-Creator Integration:** Auto-publish blog posts to Astro content collections
-**API-Ready Output:** Structured JSON for future ad platform API integration -**API-Ready Output:** Structured JSON for future ad platform API integration
-**Per-Project Context:** Context files in each website repo -**Per-Project Context:** Context files in each website repo
@@ -169,13 +168,11 @@ Output:
2. If images found: 2. If images found:
- Select best image (highest quality, product-focused) - Select best image (highest quality, product-focused)
- Call image-edit skill: - Ask user: "Use this image or provide a new one?"
prompt: "Enhance product image for {channel}, professional lighting, clean background, {channel}-specific dimensions"
3. If no images found: 3. If no images found:
- Ask user: "No product images found in repo. Please provide image path or URL." - Ask user: "No product images found in repo. Please provide image path or URL."
- Wait for user to provide - Wait for user to provide
- Then call image-edit
``` ```
#### **Non-Product Content:** #### **Non-Product Content:**
@@ -187,8 +184,8 @@ Output:
- Stats Infographic with charts - Stats Infographic with charts
- News Clean, modern announcement style - News Clean, modern announcement style
2. Call image-generation skill: 2. Ask user to provide image:
prompt: "{content_type} illustration for {topic}, {style}, Thai-friendly aesthetic, {channel}-optimized dimensions" - "For this content, please provide an image or use: [suggestion]"
3. Save images: 3. Save images:
- Social/Ads seo-multi-channel/generated-images/{topic}/{channel}/ - 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. 2. **Formality Detection:** Auto-detects from brand voice context. Defaults to casual for social, normal for blog.
3. **Image Handling:** 3. **Image Handling:**
- Product content → Browse repo first → Edit with image-edit - Product content → Browse repo first → Ask user to confirm or provide
- Non-product → Generate fresh with image-generation - Non-product → Ask user to provide image
- Blog images → Website repo - Blog images → Website repo
- Social/Ads images → Separate folder - Social/Ads images → Separate folder
@@ -617,8 +614,6 @@ Each piece of content is scored before output:
## 🔄 Integration with Other Skills ## 🔄 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 - **website-creator:** Blog posts published to Astro content collections
- **seo-analyzers:** Quality scoring and Thai language analysis - **seo-analyzers:** Quality scoring and Thai language analysis
- **seo-data:** Performance data for content optimization - **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 - ✅ Content generated for all selected channels
- ✅ Thai language processing accurate (word count, keyword density) - ✅ Thai language processing accurate (word count, keyword density)
- ✅ Product images found/enhanced or user asked to provide - ✅ Product images found or user asked to provide
- ✅ Fresh images generated for non-product content
- ✅ Blog posts published to Astro (if enabled) - ✅ Blog posts published to Astro (if enabled)
- ✅ Git commit + push successful (triggers auto-deploy) - ✅ Git commit + push successful (triggers auto-deploy)
- ✅ Output structures API-ready for future integration - ✅ Output structures API-ready for future integration

View File

@@ -19,12 +19,14 @@ import yaml
# Load environment variables # Load environment variables
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
# Thai language processing # Thai language processing
try: try:
from pythainlp import word_tokenize, sent_tokenize from pythainlp import word_tokenize, sent_tokenize
from pythainlp.util import normalize from pythainlp.util import normalize
THAI_SUPPORT = True THAI_SUPPORT = True
except ImportError: except ImportError:
THAI_SUPPORT = False THAI_SUPPORT = False
@@ -34,25 +36,25 @@ except ImportError:
class ThaiTextProcessor: class ThaiTextProcessor:
"""Thai language text processing utilities""" """Thai language text processing utilities"""
@staticmethod @staticmethod
def count_words(text: str) -> int: def count_words(text: str) -> int:
"""Count Thai words (no spaces between words)""" """Count Thai words (no spaces between words)"""
if not THAI_SUPPORT: if not THAI_SUPPORT:
return len(text.split()) return len(text.split())
tokens = word_tokenize(text, engine="newmm") tokens = word_tokenize(text, engine="newmm")
return len([t for t in tokens if t.strip() and not t.isspace()]) return len([t for t in tokens if t.strip() and not t.isspace()])
@staticmethod @staticmethod
def count_sentences(text: str) -> int: def count_sentences(text: str) -> int:
"""Count Thai sentences""" """Count Thai sentences"""
if not THAI_SUPPORT: if not THAI_SUPPORT:
return len(text.split('.')) return len(text.split("."))
sentences = sent_tokenize(text, engine="whitespace") sentences = sent_tokenize(text, engine="whitespace")
return len(sentences) return len(sentences)
@staticmethod @staticmethod
def calculate_keyword_density(text: str, keyword: str) -> float: def calculate_keyword_density(text: str, keyword: str) -> float:
"""Calculate keyword density for Thai text""" """Calculate keyword density for Thai text"""
@@ -60,124 +62,117 @@ class ThaiTextProcessor:
text_words = text.lower().split() text_words = text.lower().split()
keyword_count = text.lower().count(keyword.lower()) keyword_count = text.lower().count(keyword.lower())
return (keyword_count / len(text_words) * 100) if text_words else 0 return (keyword_count / len(text_words) * 100) if text_words else 0
text_normalized = normalize(text) text_normalized = normalize(text)
keyword_normalized = normalize(keyword) keyword_normalized = normalize(keyword)
count = text_normalized.count(keyword_normalized) count = text_normalized.count(keyword_normalized)
word_count = ThaiTextProcessor.count_words(text) word_count = ThaiTextProcessor.count_words(text)
return (count / word_count * 100) if word_count > 0 else 0 return (count / word_count * 100) if word_count > 0 else 0
@staticmethod @staticmethod
def detect_language(text: str) -> str: def detect_language(text: str) -> str:
"""Detect if content is Thai or English""" """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) total_chars = len(text)
thai_ratio = thai_chars / total_chars if total_chars > 0 else 0 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: class ChannelTemplate:
"""Load and manage channel templates""" """Load and manage channel templates"""
def __init__(self, channel_name: str, templates_dir: str): def __init__(self, channel_name: str, templates_dir: str):
self.channel_name = channel_name self.channel_name = channel_name
self.template_path = os.path.join(templates_dir, f"{channel_name}.yaml") self.template_path = os.path.join(templates_dir, f"{channel_name}.yaml")
self.template = self._load_template() self.template = self._load_template()
def _load_template(self) -> Dict: def _load_template(self) -> Dict:
"""Load YAML template""" """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) return yaml.safe_load(f)
def get_specs(self) -> Dict: def get_specs(self) -> Dict:
"""Get channel specifications""" """Get channel specifications"""
return self.template.get('fields', {}) return self.template.get("fields", {})
def get_quality_requirements(self) -> Dict: def get_quality_requirements(self) -> Dict:
"""Get quality requirements""" """Get quality requirements"""
return self.template.get('quality', {}) return self.template.get("quality", {})
class ImageHandler: class ImageHandler:
"""Handle image generation and editing""" """Handle image generation and editing"""
def __init__(self, chutes_api_token: str): def __init__(self, chutes_api_token: str):
self.chutes_token = chutes_api_token self.chutes_token = chutes_api_token
self.output_base = "output" self.output_base = "output"
def find_product_images(self, product_name: str, website_repo: str) -> List[str]: def find_product_images(self, product_name: str, website_repo: str) -> List[str]:
"""Find existing product images in website repo""" """Find existing product images in website repo"""
import glob import glob
extensions = ['.jpg', '.jpeg', '.png', '.webp'] extensions = [".jpg", ".jpeg", ".png", ".webp"]
found_images = [] found_images = []
search_patterns = [ search_patterns = [f"**/*{product_name}*{{ext}}" for ext in extensions] + [
f"**/*{product_name}*{{ext}}" for ext in extensions
] + [
"public/images/**/*{ext}", "public/images/**/*{ext}",
"src/assets/**/*{ext}" "src/assets/**/*{ext}",
] ]
for pattern in search_patterns: for pattern in search_patterns:
matches = glob.glob( matches = glob.glob(
os.path.join(website_repo, pattern.format(ext='*')), os.path.join(website_repo, pattern.format(ext="*")), recursive=True
recursive=True
) )
# Try specific extensions # Try specific extensions
for ext in extensions: for ext in extensions:
specific_matches = glob.glob( specific_matches = glob.glob(
os.path.join(website_repo, pattern.format(ext=ext)), os.path.join(website_repo, pattern.format(ext=ext)), recursive=True
recursive=True
) )
found_images.extend(specific_matches) found_images.extend(specific_matches)
return list(set(found_images))[:10] 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. Handle image for content.
For product: browse repo first, then ask user or use image-edit For product: browse repo first, ask user to confirm/provide
For non-product: generate fresh with image-generation 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") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_dir = os.path.join( output_dir = os.path.join(
self.output_base, self.output_base, self._slugify(topic), channel, "images"
self._slugify(topic),
channel,
"images"
) )
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
image_path = os.path.join(output_dir, f"generated_{timestamp}.png") image_path = os.path.join(output_dir, f"generated_{timestamp}.png")
# Placeholder - in real implementation, would call image-generation skill print(f" [Image] Please provide image for: {channel}")
print(f" [Image Generation] Would generate image for {channel}")
print(f" Topic: {topic}, Type: {content_type}") print(f" Topic: {topic}, Type: {content_type}")
return image_path return image_path
def _slugify(self, text: str) -> str: def _slugify(self, text: str) -> str:
"""Convert text to URL-friendly slug""" """Convert text to URL-friendly slug"""
import re import re
slug = re.sub(r'[^\w\s-]', '', text.lower())
slug = re.sub(r'[-\s]+', '-', slug) slug = re.sub(r"[^\w\s-]", "", text.lower())
return slug.strip('-_') slug = re.sub(r"[-\s]+", "-", slug)
return slug.strip("-_")
class ContentGenerator: class ContentGenerator:
"""Main content generator class""" """Main content generator class"""
def __init__( def __init__(
self, self,
topic: str, topic: str,
channels: List[str], channels: List[str],
website_repo: Optional[str] = None, website_repo: Optional[str] = None,
auto_publish: bool = False, auto_publish: bool = False,
language: Optional[str] = None language: Optional[str] = None,
): ):
self.topic = topic self.topic = topic
self.channels = channels self.channels = channels
@@ -186,166 +181,172 @@ class ContentGenerator:
self.language = language self.language = language
self.templates_dir = os.path.join(os.path.dirname(__file__), "templates") self.templates_dir = os.path.join(os.path.dirname(__file__), "templates")
self.output_base = "output" self.output_base = "output"
# Initialize components # Initialize components
self.text_processor = ThaiTextProcessor() self.text_processor = ThaiTextProcessor()
self.image_handler = ImageHandler(os.getenv("CHUTES_API_TOKEN", ""))
# Load templates # Load templates
self.templates = {} self.templates = {}
for channel in channels: for channel in channels:
template_name = self._get_template_name(channel) template_name = self._get_template_name(channel)
if template_name: 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]: def _get_template_name(self, channel: str) -> Optional[str]:
"""Map channel name to template file""" """Map channel name to template file"""
mapping = { mapping = {
'facebook': 'facebook', "facebook": "facebook",
'facebook_ads': 'facebook_ads', "facebook_ads": "facebook_ads",
'google_ads': 'google_ads', "google_ads": "google_ads",
'blog': 'blog', "blog": "blog",
'x': 'x_thread', "x": "x_thread",
'twitter': 'x_thread' "twitter": "x_thread",
} }
return mapping.get(channel.lower()) return mapping.get(channel.lower())
def generate_all(self) -> Dict[str, Any]: def generate_all(self) -> Dict[str, Any]:
"""Generate content for all channels""" """Generate content for all channels"""
results = { results = {
'topic': self.topic, "topic": self.topic,
'generated_at': datetime.now().isoformat(), "generated_at": datetime.now().isoformat(),
'channels': {}, "channels": {},
'summary': {} "summary": {},
} }
print(f"\n🎯 Generating content for: {self.topic}") print(f"\n🎯 Generating content for: {self.topic}")
print(f"📱 Channels: {', '.join(self.channels)}") print(f"📱 Channels: {', '.join(self.channels)}")
print(f"🌐 Language: {self.language or 'auto-detect'}\n") print(f"🌐 Language: {self.language or 'auto-detect'}\n")
for channel in self.channels: for channel in self.channels:
if channel in self.templates: if channel in self.templates:
print(f" Generating {channel}...") print(f" Generating {channel}...")
channel_result = self._generate_for_channel(channel) channel_result = self._generate_for_channel(channel)
results['channels'][channel] = channel_result results["channels"][channel] = channel_result
# Save results # Save results
self._save_results(results) self._save_results(results)
return results return results
def _generate_for_channel(self, channel: str) -> Dict: def _generate_for_channel(self, channel: str) -> Dict:
"""Generate content for specific channel""" """Generate content for specific channel"""
template = self.templates[channel] template = self.templates[channel]
specs = template.get_specs() specs = template.get_specs()
# Detect language from topic # Detect language from topic
lang = self.language or self.text_processor.detect_language(self.topic) lang = self.language or self.text_processor.detect_language(self.topic)
# Generate variations (placeholder - real implementation would use LLM) # Generate variations (placeholder - real implementation would use LLM)
variations = [] 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): for i in range(num_variations):
variation = self._create_variation(channel, i, lang, specs) variation = self._create_variation(channel, i, lang, specs)
variations.append(variation) variations.append(variation)
return { return {
'channel': channel, "channel": channel,
'language': lang, "language": lang,
'variations': variations, "variations": variations,
'api_ready': template.template.get('api_ready', False) "api_ready": template.template.get("api_ready", False),
} }
def _create_variation( def _create_variation(
self, self, channel: str, variation_num: int, language: str, specs: Dict
channel: str,
variation_num: int,
language: str,
specs: Dict
) -> Dict: ) -> Dict:
"""Create single content variation""" """Create single content variation"""
# This is a placeholder - real implementation would call LLM # This is a placeholder - real implementation would call LLM
# with proper prompts based on channel template # with proper prompts based on channel template
base_variation = { base_variation = {
'id': f"{channel}_var_{variation_num + 1}", "id": f"{channel}_var_{variation_num + 1}",
'created_at': datetime.now().isoformat() "created_at": datetime.now().isoformat(),
} }
# Channel-specific structure # Channel-specific structure
if channel == 'facebook': if channel == "facebook":
base_variation.update({ base_variation.update(
'primary_text': f"[Facebook Post {variation_num + 1}] {self.topic}...", {
'headline': f"[Headline] {self.topic}", "primary_text": f"[Facebook Post {variation_num + 1}] {self.topic}...",
'cta': "เรียนรู้เพิ่มเติม" if language == 'th' else "Learn More", "headline": f"[Headline] {self.topic}",
'hashtags': [f"#{self.topic.replace(' ', '')}"], "cta": "เรียนรู้เพิ่มเติม" if language == "th" else "Learn More",
'image': { "hashtags": [f"#{self.topic.replace(' ', '')}"],
'path': self.image_handler.generate_image_for_channel( "image": {
self.topic, channel, 'social' "path": self.generate_image_for_channel(
) self.topic, channel, "social"
)
},
} }
}) )
elif channel == 'facebook_ads': elif channel == "facebook_ads":
base_variation.update({ base_variation.update(
'primary_text': f"[FB Ad Primary Text] {self.topic}...", {
'headline': f"[FB Ad Headline - 40 chars]", "primary_text": f"[FB Ad Primary Text] {self.topic}...",
'description': f"[FB Ad Description - 90 chars]", "headline": f"[FB Ad Headline - 40 chars]",
'cta': "SHOP_NOW", "description": f"[FB Ad Description - 90 chars]",
'api_ready': { "cta": "SHOP_NOW",
'platform': 'meta', "api_ready": {
'api_version': 'v18.0', "platform": "meta",
'endpoint': '/act_{ad_account_id}/adcreatives' "api_version": "v18.0",
"endpoint": "/act_{ad_account_id}/adcreatives",
},
} }
}) )
elif channel == 'google_ads': elif channel == "google_ads":
base_variation.update({ base_variation.update(
'headlines': [ {
{'text': f"[Headline {i+1}] {self.topic}"} "headlines": [
for i in range(15) {"text": f"[Headline {i + 1}] {self.topic}"} for i in range(15)
], ],
'descriptions': [ "descriptions": [
{'text': f"[Description {i+1}] Learn more about {self.topic}"} {"text": f"[Description {i + 1}] Learn more about {self.topic}"}
for i in range(4) for i in range(4)
], ],
'keywords': [self.topic, f"บริการ {self.topic}"], "keywords": [self.topic, f"บริการ {self.topic}"],
'api_ready': { "api_ready": {
'platform': 'google', "platform": "google",
'api_version': 'v15.0', "api_version": "v15.0",
'endpoint': '/google.ads.googleads.v15.services/GoogleAdsService:Mutate' "endpoint": "/google.ads.googleads.v15.services/GoogleAdsService:Mutate",
},
} }
}) )
elif channel == 'blog': elif channel == "blog":
base_variation.update({ base_variation.update(
'markdown': self._generate_blog_markdown(language), {
'frontmatter': { "markdown": self._generate_blog_markdown(language),
'title': f"{self.topic} - Complete Guide", "frontmatter": {
'description': f"Learn about {self.topic}", "title": f"{self.topic} - Complete Guide",
'slug': self._slugify(self.topic), "description": f"Learn about {self.topic}",
'lang': language "slug": self._slugify(self.topic),
}, "lang": language,
'word_count': 2000 if language == 'en' else 1500, },
'publish_status': 'draft' "word_count": 2000 if language == "en" else 1500,
}) "publish_status": "draft",
}
elif channel in ['x', 'twitter']: )
base_variation.update({
'tweets': [ elif channel in ["x", "twitter"]:
f"[Tweet {i+1}/7] Content about {self.topic}..." base_variation.update(
for i in range(7) {
], "tweets": [
'thread_title': f"Everything about {self.topic} 🧵" f"[Tweet {i + 1}/7] Content about {self.topic}..."
}) for i in range(7)
],
"thread_title": f"Everything about {self.topic} 🧵",
}
)
return base_variation return base_variation
def _generate_blog_markdown(self, language: str) -> str: def _generate_blog_markdown(self, language: str) -> str:
"""Generate blog post in Markdown format""" """Generate blog post in Markdown format"""
slug = self._slugify(self.topic) slug = self._slugify(self.topic)
markdown = f"""--- markdown = f"""---
title: "{self.topic} - Complete Guide" title: "{self.topic} - Complete Guide"
description: "Learn everything about {self.topic} in this comprehensive guide" description: "Learn everything about {self.topic} in this comprehensive guide"
@@ -354,7 +355,7 @@ slug: {slug}
lang: {language} lang: {language}
category: guides category: guides
tags: ["{self.topic}", "guide"] tags: ["{self.topic}", "guide"]
created: {datetime.now().strftime('%Y-%m-%d')} created: {datetime.now().strftime("%Y-%m-%d")}
--- ---
# {self.topic}: Complete Guide # {self.topic}: Complete Guide
@@ -384,95 +385,91 @@ created: {datetime.now().strftime('%Y-%m-%d')}
[Summary and call-to-action...] [Summary and call-to-action...]
""" """
return markdown return markdown
def _save_results(self, results: Dict): def _save_results(self, results: Dict):
"""Save results to output directory""" """Save results to output directory"""
output_dir = os.path.join( output_dir = os.path.join(self.output_base, self._slugify(self.topic))
self.output_base,
self._slugify(self.topic)
)
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
output_file = os.path.join(output_dir, "results.json") 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) json.dump(results, f, indent=2, ensure_ascii=False)
print(f"\n✅ Results saved to: {output_file}") print(f"\n✅ Results saved to: {output_file}")
def _slugify(self, text: str) -> str: def _slugify(self, text: str) -> str:
"""Convert text to URL-friendly slug""" """Convert text to URL-friendly slug"""
import re import re
slug = re.sub(r'[^\w\s-]', '', text.lower())
slug = re.sub(r'[-\s]+', '-', slug) slug = re.sub(r"[^\w\s-]", "", text.lower())
return slug.strip('-_') slug = re.sub(r"[-\s]+", "-", slug)
return slug.strip("-_")
def main(): def main():
"""Main entry point""" """Main entry point"""
parser = argparse.ArgumentParser( 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( parser.add_argument(
'--topic', '-t', "--topic", "-t", required=True, help="Topic to generate content about"
required=True,
help='Topic to generate content about'
) )
parser.add_argument( parser.add_argument(
'--channels', '-c', "--channels",
nargs='+', "-c",
default=['facebook', 'facebook_ads', 'google_ads', 'blog', 'x'], nargs="+",
choices=['facebook', 'facebook_ads', 'google_ads', 'blog', 'x', 'twitter'], default=["facebook", "facebook_ads", "google_ads", "blog", "x"],
help='Channels to generate content for' choices=["facebook", "facebook_ads", "google_ads", "blog", "x", "twitter"],
help="Channels to generate content for",
) )
parser.add_argument( parser.add_argument(
'--website-repo', '-w', "--website-repo",
help='Path to website repository (for blog auto-publish)' "-w",
help="Path to website repository (for blog auto-publish)",
) )
parser.add_argument( parser.add_argument(
'--auto-publish', "--auto-publish", action="store_true", help="Auto-publish blog posts to website"
action='store_true',
help='Auto-publish blog posts to website'
) )
parser.add_argument( parser.add_argument(
'--language', '-l', "--language",
choices=['th', 'en'], "-l",
help='Content language (default: auto-detect)' choices=["th", "en"],
help="Content language (default: auto-detect)",
) )
parser.add_argument( parser.add_argument(
'--product-name', '-p', "--product-name", "-p", help="Product name (for product image handling)"
help='Product name (for product image handling)'
) )
args = parser.parse_args() args = parser.parse_args()
# Create generator # Create generator
generator = ContentGenerator( generator = ContentGenerator(
topic=args.topic, topic=args.topic,
channels=args.channels, channels=args.channels,
website_repo=args.website_repo, website_repo=args.website_repo,
auto_publish=args.auto_publish, auto_publish=args.auto_publish,
language=args.language language=args.language,
) )
# Generate content # Generate content
results = generator.generate_all() results = generator.generate_all()
# Print summary # Print summary
print("\n📊 Summary:") print("\n📊 Summary:")
print(f" Topic: {results['topic']}") print(f" Topic: {results['topic']}")
print(f" Channels generated: {len(results['channels'])}") 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" - {channel}: {len(data['variations'])} variations")
print(f"\n✨ Done!") print(f"\n✨ Done!")
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -2,312 +2,123 @@
""" """
Image Integration Module Image Integration Module
Integrates with image-generation and image-edit skills.
Handles product vs non-product image workflows. 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 os
import sys import glob
import subprocess
import argparse import argparse
from pathlib import Path
from typing import Optional, List from typing import Optional, List
class ImageIntegration: class ImageIntegration:
"""Integrate with image-generation and image-edit skills""" def __init__(self, skills_base_path: str = ""):
pass
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 find_product_images(self, product_name: str, website_repo: str) -> List[str]: def find_product_images(self, product_name: str, website_repo: str) -> List[str]:
""" """
Find existing product images in website repo Find existing product images in website repo
Args: Args:
product_name: Product name to search for product_name: Product name to search for
website_repo: Path to website repository website_repo: Path to website repository
Returns: Returns:
List of image paths 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 = [] found_images = []
# Search patterns
patterns = [ patterns = [
f"**/*{product_name}*{{ext}}", f"**/*{product_name}*{{ext}}",
f"public/images/**/*{{ext}}", f"public/images/**/*{{ext}}",
f"src/assets/**/*{{ext}}" f"src/assets/**/*{{ext}}",
] ]
for pattern in patterns: for pattern in patterns:
for ext in extensions: for ext in extensions:
search_pattern = pattern.format(ext=ext) search_pattern = pattern.format(ext=ext)
matches = glob.glob(os.path.join(website_repo, search_pattern), recursive=True) matches = glob.glob(
found_images.extend(matches[:5]) # Limit per pattern os.path.join(website_repo, search_pattern), recursive=True
)
return list(set(found_images))[:10] # Return unique, max 10 found_images.extend(matches[:5])
def handle_product_content(self, product_name: str, website_repo: str, return list(set(found_images))[:10]
edit_prompt: str, output_dir: str,
topic: str, channel: str) -> Optional[str]: def handle_product_content(
""" self, product_name: str, website_repo: str
Handle image for product content ) -> Optional[List[str]]:
"""Handle image for product content - returns found images for user to select"""
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
"""
print(f"\n🔍 Looking for product images: {product_name}") print(f"\n🔍 Looking for product images: {product_name}")
# Step 1: Find existing images
images = self.find_product_images(product_name, website_repo) images = self.find_product_images(product_name, website_repo)
if images: if images:
print(f" ✓ Found {len(images)} image(s)") print(f" ✓ Found {len(images)} image(s):")
best_image = images[0] # Use first/best match for i, img in enumerate(images[:5], 1):
print(f" {i}. {img}")
# Step 2: Edit image return images
return self.edit_product_image(
best_image,
edit_prompt,
output_dir,
topic,
channel
)
else: else:
print(f" ✗ No product images found in repo") print(f" ✗ No product images found in repo")
print(f" Please provide product image manually")
return None return None
def handle_non_product_content(self, content_type: str, topic: str, def suggest_non_product_image(self, content_type: str, topic: str) -> str:
output_dir: str, channel: str) -> Optional[str]:
""" """
Generate fresh image for non-product content Suggest image for non-product content
Args: Args:
content_type: Type (service, stats, knowledge) content_type: Type (service, stats, knowledge)
topic: Topic name topic: Topic name
output_dir: Output directory
channel: Channel name
Returns: Returns:
Path to generated image Suggestion message
""" """
# Create prompt based on content type suggestions = {
prompts = { "service": f"Please provide a professional service illustration for: {topic}",
'service': f"Professional illustration of {topic}, modern flat design, business context, Thai-friendly aesthetic", "stats": f"Please provide a data visualization/infographic image for: {topic}",
'stats': f"Data visualization infographic for {topic}, clean charts, professional style", "knowledge": f"Please provide an educational illustration for: {topic}",
'knowledge': f"Educational illustration for {topic}, clear visual metaphor, engaging style", "default": f"Please provide an image for: {topic}",
'default': f"Professional image for {topic}, modern design, high quality"
} }
prompt = prompts.get(content_type, prompts['default']) return suggestions.get(content_type, suggestions["default"])
# Generate image
return self.generate_image(
prompt,
output_dir,
topic=topic,
channel=channel
)
def main(): def main():
"""Test image integration""" """Test image integration"""
parser = argparse.ArgumentParser(description='Test Image Integration') parser = argparse.ArgumentParser(description="Test Image Integration")
parser.add_argument('--action', choices=['generate', 'edit', 'find'], required=True) parser.add_argument("--action", choices=["find", "suggest"], required=True)
parser.add_argument('--prompt', help='Image prompt or edit instructions') parser.add_argument("--topic", help="Topic name")
parser.add_argument('--topic', help='Topic name') parser.add_argument("--product-name", help="Product name (for find action)")
parser.add_argument('--channel', help='Channel name') parser.add_argument("--website-repo", help="Website repo path (for find action)")
parser.add_argument('--output-dir', default='./output', help='Output directory') parser.add_argument(
parser.add_argument('--product-name', help='Product name (for find action)') "--content-type", default="default", help="Content type (for suggest action)"
parser.add_argument('--website-repo', help='Website repo path (for find action)') )
args = parser.parse_args() args = parser.parse_args()
integration = ImageIntegration() integration = ImageIntegration()
if args.action == 'generate': if args.action == "find":
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 not args.product_name or not args.website_repo: if not args.product_name or not args.website_repo:
print("Error: --product-name and --website-repo required for find") print("Error: --product-name and --website-repo required for find")
return return
images = integration.find_product_images(args.product_name, args.website_repo) images = integration.find_product_images(args.product_name, args.website_repo)
print(f"\nFound {len(images)} images:") print(f"\nFound {len(images)} images:")
for img in images: for img in images:
print(f" - {img}") 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() main()