feat(v3): PR 3 — prompt_layer package (base, video_prompt, search_query, visual_vocabulary)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
43
bots/prompt_layer/__init__.py
Normal file
43
bots/prompt_layer/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
bots/prompt_layer/__init__.py
|
||||||
|
Unified entry point for all prompt composition.
|
||||||
|
|
||||||
|
V3.0 scope: video + search + tts categories
|
||||||
|
V3.1+: expand to all categories
|
||||||
|
"""
|
||||||
|
from .base import ComposedPrompt
|
||||||
|
from .video_prompt import KlingPromptFormatter, VeoPromptFormatter
|
||||||
|
from .search_query import StockSearchQueryComposer
|
||||||
|
|
||||||
|
|
||||||
|
def compose(category: str, input_data: dict, engine: str) -> 'ComposedPrompt':
|
||||||
|
"""
|
||||||
|
Unified entry point for all prompt composition.
|
||||||
|
|
||||||
|
category: 'video' | 'search' | 'tts' | 'image' | 'writing' | 'caption'
|
||||||
|
input_data: category-specific dict
|
||||||
|
engine: target engine name
|
||||||
|
|
||||||
|
V3.0 scope: video + search only
|
||||||
|
V3.1+: expand to all categories
|
||||||
|
"""
|
||||||
|
composer = _get_composer(category, engine)
|
||||||
|
return composer.compose(input_data, engine)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_composer(category: str, engine: str):
|
||||||
|
"""Return appropriate composer for category+engine combination."""
|
||||||
|
if category == 'video':
|
||||||
|
if engine in ('kling_free', 'kling_pro'):
|
||||||
|
return KlingPromptFormatter()
|
||||||
|
else:
|
||||||
|
return VeoPromptFormatter()
|
||||||
|
elif category == 'search':
|
||||||
|
return StockSearchQueryComposer()
|
||||||
|
else:
|
||||||
|
# Fallback: return a passthrough composer for unsupported categories
|
||||||
|
from .base import PassthroughComposer
|
||||||
|
return PassthroughComposer()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['compose', 'ComposedPrompt']
|
||||||
41
bots/prompt_layer/base.py
Normal file
41
bots/prompt_layer/base.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
bots/prompt_layer/base.py
|
||||||
|
Base types for the prompt layer.
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ComposedPrompt:
|
||||||
|
"""
|
||||||
|
Unified prompt container returned by all composers.
|
||||||
|
|
||||||
|
Fields used varies by engine:
|
||||||
|
- Kling: positive + negative
|
||||||
|
- Veo: positive (structured)
|
||||||
|
- Search: queries list
|
||||||
|
- TTS: processed_text
|
||||||
|
"""
|
||||||
|
positive: str = ''
|
||||||
|
negative: str = ''
|
||||||
|
queries: list[str] = field(default_factory=list)
|
||||||
|
processed_text: str = ''
|
||||||
|
metadata: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
return bool(self.positive or self.queries or self.processed_text)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseComposer:
|
||||||
|
"""Abstract base for all composers."""
|
||||||
|
def compose(self, input_data: dict, engine: str) -> ComposedPrompt:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class PassthroughComposer(BaseComposer):
|
||||||
|
"""Returns input as-is for unsupported categories."""
|
||||||
|
def compose(self, input_data: dict, engine: str) -> ComposedPrompt:
|
||||||
|
return ComposedPrompt(
|
||||||
|
positive=input_data.get('text', ''),
|
||||||
|
metadata={'passthrough': True, 'engine': engine}
|
||||||
|
)
|
||||||
55
bots/prompt_layer/search_query.py
Normal file
55
bots/prompt_layer/search_query.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
bots/prompt_layer/search_query.py
|
||||||
|
Compose stock video/image search queries from Korean concepts.
|
||||||
|
"""
|
||||||
|
from .base import BaseComposer, ComposedPrompt
|
||||||
|
from .visual_vocabulary import CONCEPT_TO_VISUAL, VISUAL_STYLE_MODIFIERS
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class StockSearchQueryComposer(BaseComposer):
|
||||||
|
"""
|
||||||
|
Korean concept -> English visual search terms.
|
||||||
|
Used to search Pexels/Pixabay/Unsplash for stock footage.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compose(self, input_data: dict, engine: str = 'pexels') -> ComposedPrompt:
|
||||||
|
"""
|
||||||
|
input_data: {
|
||||||
|
'sentence': str, # Korean sentence to find visuals for
|
||||||
|
'platform': str, # 'pexels' | 'pixabay' | 'kling' | 'veo'
|
||||||
|
'count': int, # number of search queries to return (default 3)
|
||||||
|
}
|
||||||
|
Returns ComposedPrompt with queries list
|
||||||
|
"""
|
||||||
|
sentence = input_data.get('sentence', '')
|
||||||
|
count = input_data.get('count', 3)
|
||||||
|
|
||||||
|
queries = self._sentence_to_queries(sentence, count)
|
||||||
|
|
||||||
|
return ComposedPrompt(
|
||||||
|
queries=queries,
|
||||||
|
metadata={'sentence': sentence, 'engine': engine}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _sentence_to_queries(self, sentence: str, count: int) -> list[str]:
|
||||||
|
"""Extract Korean concepts from sentence and map to visual search terms."""
|
||||||
|
# Find matching concepts from vocabulary
|
||||||
|
matched_visuals = []
|
||||||
|
for concept, visuals in CONCEPT_TO_VISUAL.items():
|
||||||
|
if concept in sentence:
|
||||||
|
matched_visuals.extend(visuals)
|
||||||
|
|
||||||
|
# If no matches, use generic professional stock footage terms
|
||||||
|
if not matched_visuals:
|
||||||
|
matched_visuals = ['professional business', 'modern lifestyle', 'technology future']
|
||||||
|
|
||||||
|
# Return up to count unique queries
|
||||||
|
seen = set()
|
||||||
|
unique = []
|
||||||
|
for v in matched_visuals:
|
||||||
|
if v not in seen:
|
||||||
|
seen.add(v)
|
||||||
|
unique.append(v)
|
||||||
|
|
||||||
|
return unique[:count]
|
||||||
85
bots/prompt_layer/video_prompt.py
Normal file
85
bots/prompt_layer/video_prompt.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"""
|
||||||
|
bots/prompt_layer/video_prompt.py
|
||||||
|
Format prompts for video generation engines (Kling, Veo).
|
||||||
|
"""
|
||||||
|
from .base import BaseComposer, ComposedPrompt
|
||||||
|
from .visual_vocabulary import VISUAL_STYLE_MODIFIERS, NEGATIVE_TERMS
|
||||||
|
|
||||||
|
|
||||||
|
class KlingPromptFormatter(BaseComposer):
|
||||||
|
"""
|
||||||
|
Format prompts for Kling AI video generation.
|
||||||
|
Kling works best with: scene description + movement + mood + negative prompt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compose(self, input_data: dict, engine: str = 'kling_free') -> ComposedPrompt:
|
||||||
|
"""
|
||||||
|
input_data: {
|
||||||
|
'scenes': list[dict], # [{text, type, image_prompt}, ...]
|
||||||
|
'corner': str, # content corner/category
|
||||||
|
'duration': float, # target duration in seconds
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
scenes = input_data.get('scenes', [])
|
||||||
|
corner = input_data.get('corner', '')
|
||||||
|
|
||||||
|
# Build positive prompt from scenes
|
||||||
|
scene_texts = []
|
||||||
|
for scene in scenes:
|
||||||
|
prompt = scene.get('image_prompt') or scene.get('text', '')
|
||||||
|
if prompt:
|
||||||
|
scene_texts.append(self._enhance_for_kling(prompt, corner))
|
||||||
|
|
||||||
|
positive = '. '.join(scene_texts[:3]) # Max 3 scenes per prompt
|
||||||
|
if not positive:
|
||||||
|
positive = f'cinematic short video about {corner or "technology"}'
|
||||||
|
|
||||||
|
# Kling negative prompt
|
||||||
|
negative = ', '.join(NEGATIVE_TERMS + ['text overlay', 'subtitles', 'watermark'])
|
||||||
|
|
||||||
|
# Add beat markers for Kling
|
||||||
|
positive = f'{positive}. Camera: smooth movement, vertical 9:16 format. Style: cinematic, vibrant.'
|
||||||
|
|
||||||
|
return ComposedPrompt(
|
||||||
|
positive=positive,
|
||||||
|
negative=negative,
|
||||||
|
metadata={'engine': engine, 'corner': corner}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _enhance_for_kling(self, text: str, corner: str) -> str:
|
||||||
|
"""Add cinematic enhancement to prompt."""
|
||||||
|
modifiers = ', '.join(VISUAL_STYLE_MODIFIERS[:3])
|
||||||
|
return f'{text}, {modifiers}'
|
||||||
|
|
||||||
|
|
||||||
|
class VeoPromptFormatter(BaseComposer):
|
||||||
|
"""
|
||||||
|
Format prompts for Google Veo video generation.
|
||||||
|
Veo works best with structured ingredient list format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compose(self, input_data: dict, engine: str = 'veo3') -> ComposedPrompt:
|
||||||
|
"""
|
||||||
|
input_data: same as KlingPromptFormatter
|
||||||
|
"""
|
||||||
|
scenes = input_data.get('scenes', [])
|
||||||
|
corner = input_data.get('corner', '')
|
||||||
|
|
||||||
|
scene_texts = [
|
||||||
|
scene.get('image_prompt') or scene.get('text', '')
|
||||||
|
for scene in scenes if scene.get('image_prompt') or scene.get('text')
|
||||||
|
]
|
||||||
|
|
||||||
|
# Veo structured format: Subject + Action + Setting + Style
|
||||||
|
subject = scene_texts[0] if scene_texts else f'{corner or "technology"} concept'
|
||||||
|
positive = (
|
||||||
|
f'Subject: {subject}. '
|
||||||
|
f'Format: vertical 9:16 portrait video. '
|
||||||
|
f'Style: cinematic, {", ".join(VISUAL_STYLE_MODIFIERS[:2])}. '
|
||||||
|
f'Camera: smooth pan or zoom. Duration: short clip.'
|
||||||
|
)
|
||||||
|
|
||||||
|
return ComposedPrompt(
|
||||||
|
positive=positive,
|
||||||
|
metadata={'engine': engine, 'corner': corner, 'format': 'veo_structured'}
|
||||||
|
)
|
||||||
145
bots/prompt_layer/visual_vocabulary.py
Normal file
145
bots/prompt_layer/visual_vocabulary.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
"""
|
||||||
|
bots/prompt_layer/visual_vocabulary.py
|
||||||
|
Shared Korean -> English visual concept dictionary.
|
||||||
|
Used by search_query.py and video_prompt.py for concept mapping.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CONCEPT_TO_VISUAL = {
|
||||||
|
# Technology
|
||||||
|
'AI': ['artificial intelligence screen', 'digital interface', 'neural network visualization'],
|
||||||
|
'인공지능': ['robot brain', 'digital mind', 'AI hologram'],
|
||||||
|
'자동화': ['gears mechanism', 'conveyor belt', 'robot arm factory'],
|
||||||
|
'코딩': ['computer code screen', 'programmer keyboard', 'dark terminal code'],
|
||||||
|
'데이터': ['data visualization', 'bar chart analytics', 'network nodes'],
|
||||||
|
'알고리즘': ['flowchart diagram', 'binary code', 'decision tree'],
|
||||||
|
'앱': ['smartphone screen', 'mobile app interface', 'app store'],
|
||||||
|
'소프트웨어': ['software development', 'code editor', 'programming laptop'],
|
||||||
|
# Finance/Money
|
||||||
|
'돈': ['money cash bills', 'coins pile', 'dollar bills'],
|
||||||
|
'수익': ['profit growth chart', 'rising arrow money', 'income cash'],
|
||||||
|
'투자': ['stock market chart', 'investment portfolio', 'financial growth'],
|
||||||
|
'절약': ['piggy bank savings', 'money jar coins', 'budget planning'],
|
||||||
|
'부자': ['luxury lifestyle', 'wealthy business person', 'success achievement'],
|
||||||
|
'무료': ['gift present box', 'unlocked padlock', 'free tag label'],
|
||||||
|
'할인': ['sale discount tag', 'percent off sign', 'price reduction'],
|
||||||
|
# Business
|
||||||
|
'비즈니스': ['business meeting', 'office workspace', 'professional handshake'],
|
||||||
|
'창업': ['startup launch rocket', 'entrepreneur office', 'business idea lightbulb'],
|
||||||
|
'마케팅': ['marketing strategy board', 'social media icons', 'advertising billboard'],
|
||||||
|
'브랜드': ['brand logo design', 'brand identity', 'premium label'],
|
||||||
|
'고객': ['customer service smile', 'client meeting', 'happy customer'],
|
||||||
|
'성공': ['success achievement trophy', 'winner podium', 'goal celebration'],
|
||||||
|
'실패': ['failure mistake frustrated', 'broken plan', 'problem obstacle'],
|
||||||
|
# Health/Lifestyle
|
||||||
|
'건강': ['healthy lifestyle', 'fitness exercise', 'fresh vegetables'],
|
||||||
|
'다이어트': ['diet food salad', 'weight loss scale', 'healthy eating'],
|
||||||
|
'운동': ['gym workout exercise', 'running sport', 'fitness training'],
|
||||||
|
'수면': ['peaceful sleep bedroom', 'sleeping person night', 'rest relaxation'],
|
||||||
|
'스트레스': ['stress anxiety person', 'overwhelmed work', 'headache pressure'],
|
||||||
|
'행복': ['happy smiling person', 'joy celebration', 'positive energy'],
|
||||||
|
# Education
|
||||||
|
'공부': ['studying books desk', 'student learning', 'open textbook'],
|
||||||
|
'독서': ['reading book cozy', 'bookshelf library', 'person reading'],
|
||||||
|
'교육': ['classroom teaching', 'education school', 'learning knowledge'],
|
||||||
|
'자격증': ['certificate diploma award', 'achievement credential', 'professional certification'],
|
||||||
|
# Social/Communication
|
||||||
|
'소통': ['communication talking', 'conversation speech bubble', 'people talking'],
|
||||||
|
'관계': ['relationship people together', 'friendship bond', 'social connection'],
|
||||||
|
'가족': ['family together happy', 'family portrait', 'home family'],
|
||||||
|
'친구': ['friends together laughing', 'friendship bond', 'social gathering'],
|
||||||
|
# Environment/Nature
|
||||||
|
'자연': ['nature landscape scenic', 'green forest trees', 'outdoor beauty'],
|
||||||
|
'환경': ['environment ecology', 'green earth planet', 'sustainability'],
|
||||||
|
'도시': ['city skyline urban', 'modern architecture', 'downtown cityscape'],
|
||||||
|
'여행': ['travel adventure journey', 'wanderlust explore', 'tourism destination'],
|
||||||
|
# Time/Productivity
|
||||||
|
'시간': ['clock time management', 'hourglass countdown', 'calendar schedule'],
|
||||||
|
'생산성': ['productivity work desk', 'efficient workflow', 'organized workspace'],
|
||||||
|
'습관': ['habit routine daily', 'calendar habit tracker', 'consistent practice'],
|
||||||
|
'목표': ['goal target arrow', 'achievement milestone', 'success roadmap'],
|
||||||
|
# Food
|
||||||
|
'음식': ['food meal delicious', 'restaurant dining', 'cooking kitchen'],
|
||||||
|
'커피': ['coffee cup cafe', 'espresso morning', 'coffee shop cozy'],
|
||||||
|
'요리': ['cooking chef kitchen', 'recipe preparation', 'homemade food'],
|
||||||
|
# Digital/Social Media
|
||||||
|
'유튜브': ['youtube play button', 'video content creator', 'streaming platform'],
|
||||||
|
'틱톡': ['social media video', 'short video content', 'viral content'],
|
||||||
|
'인스타그램': ['instagram photo aesthetic', 'social media post', 'influencer lifestyle'],
|
||||||
|
'콘텐츠': ['content creation studio', 'digital content', 'creative media'],
|
||||||
|
# Generic actions
|
||||||
|
'시작': ['starting launch beginning', 'new start fresh', 'launch rocket'],
|
||||||
|
'변화': ['change transformation', 'before after contrast', 'evolution progress'],
|
||||||
|
'성장': ['growth plant sprouting', 'growth chart rising', 'development progress'],
|
||||||
|
'문제': ['problem solving puzzle', 'challenge obstacle', 'issue question mark'],
|
||||||
|
'해결': ['solution lightbulb', 'problem solved checkmark', 'resolution answer'],
|
||||||
|
'비교': ['comparison side by side', 'versus contrast', 'pros cons balance'],
|
||||||
|
'순위': ['ranking top list', 'leaderboard winners', 'chart comparison'],
|
||||||
|
'방법': ['how-to guide steps', 'tutorial instruction', 'method process'],
|
||||||
|
'팁': ['tips tricks advice', 'helpful hints', 'pro tip star'],
|
||||||
|
'비밀': ['secret reveal hidden', 'mystery unlock', 'insider knowledge'],
|
||||||
|
'진실': ['truth reveal facts', 'reality check', 'honest disclosure'],
|
||||||
|
'놀라운': ['surprising amazing wow', 'unexpected revelation', 'shocking discovery'],
|
||||||
|
# Numbers/Stats
|
||||||
|
'1위': ['number one winner', 'first place gold', 'top ranked best'],
|
||||||
|
'100%': ['one hundred percent complete', 'full capacity', 'perfect score'],
|
||||||
|
# Korean culture
|
||||||
|
'한국': ['korea seoul cityscape', 'korean culture', 'hanbok traditional'],
|
||||||
|
'직장': ['office workplace corporate', 'work desk professional', 'business office'],
|
||||||
|
'취업': ['job interview hiring', 'employment opportunity', 'career success'],
|
||||||
|
'부동산': ['real estate property', 'house home investment', 'property market'],
|
||||||
|
# Abstract concepts
|
||||||
|
'가능성': ['possibility open door', 'opportunity horizon', 'potential unlimited'],
|
||||||
|
'미래': ['future technology vision', 'futuristic landscape', 'innovation tomorrow'],
|
||||||
|
'트렌드': ['trend arrow upward', 'trending popular', 'hot topic social'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Quality/style modifiers to append to video/image prompts
|
||||||
|
VISUAL_STYLE_MODIFIERS = [
|
||||||
|
'cinematic',
|
||||||
|
'4k',
|
||||||
|
'professional',
|
||||||
|
'high quality',
|
||||||
|
'vibrant colors',
|
||||||
|
'sharp focus',
|
||||||
|
'natural lighting',
|
||||||
|
'smooth motion',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Terms to avoid in video generation prompts
|
||||||
|
NEGATIVE_TERMS = [
|
||||||
|
'blurry',
|
||||||
|
'low quality',
|
||||||
|
'watermark',
|
||||||
|
'text overlay',
|
||||||
|
'distorted',
|
||||||
|
'pixelated',
|
||||||
|
'grainy',
|
||||||
|
'overexposed',
|
||||||
|
'underexposed',
|
||||||
|
'shaky camera',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if '--test' in sys.argv:
|
||||||
|
print('=== visual_vocabulary 테스트 시작 ===')
|
||||||
|
print(f'총 개념 수: {len(CONCEPT_TO_VISUAL)}')
|
||||||
|
print(f'스타일 수식어 수: {len(VISUAL_STYLE_MODIFIERS)}')
|
||||||
|
print(f'네거티브 용어 수: {len(NEGATIVE_TERMS)}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test a few lookups
|
||||||
|
test_concepts = ['AI', '미래', '성공', '건강', '코딩']
|
||||||
|
for concept in test_concepts:
|
||||||
|
visuals = CONCEPT_TO_VISUAL.get(concept, [])
|
||||||
|
print(f' [{concept}] -> {visuals}')
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f'스타일 수식어: {VISUAL_STYLE_MODIFIERS}')
|
||||||
|
print(f'네거티브 용어: {NEGATIVE_TERMS}')
|
||||||
|
print()
|
||||||
|
print('=== 테스트 완료 ===')
|
||||||
|
else:
|
||||||
|
print('사용법: python -m bots.prompt_layer.visual_vocabulary --test')
|
||||||
Reference in New Issue
Block a user