Compare commits
6 Commits
24534ccb3e
...
6301186288
| Author | SHA1 | Date |
|---|---|---|
|
|
6301186288 | |
|
|
f7dba437cf | |
|
|
68369b64de | |
|
|
d955ac80f1 | |
|
|
ae9f0b3c62 | |
|
|
f8c1738aa2 |
|
|
@ -52,3 +52,4 @@ Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
|
||||||
zzz/
|
zzz/
|
||||||
|
credentials/service_account.json
|
||||||
|
|
@ -314,7 +314,10 @@ def add_exception_handlers(app: FastAPI):
|
||||||
|
|
||||||
@app.exception_handler(DashboardException)
|
@app.exception_handler(DashboardException)
|
||||||
def dashboard_exception_handler(request: Request, exc: DashboardException) -> Response:
|
def dashboard_exception_handler(request: Request, exc: DashboardException) -> Response:
|
||||||
logger.debug(f"Handled DashboardException: {exc.__class__.__name__} - {exc.message}")
|
if exc.status_code < 500:
|
||||||
|
logger.warning(f"Handled DashboardException: {exc.__class__.__name__} - {exc.message}")
|
||||||
|
else:
|
||||||
|
logger.error(f"Handled DashboardException: {exc.__class__.__name__} - {exc.message}")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=exc.status_code,
|
status_code=exc.status_code,
|
||||||
content={
|
content={
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,9 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
try:
|
try:
|
||||||
yield session
|
yield session
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
from app.dashboard.exceptions import DashboardException
|
||||||
|
if isinstance(e, DashboardException):
|
||||||
|
raise e
|
||||||
import traceback
|
import traceback
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import os, json
|
import gspread
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from google.oauth2.service_account import Credentials
|
||||||
from config import prompt_settings
|
from config import prompt_settings
|
||||||
from app.utils.logger import get_logger
|
from app.utils.logger import get_logger
|
||||||
from app.utils.prompts.schemas import *
|
from app.utils.prompts.schemas import *
|
||||||
|
|
@ -7,29 +8,37 @@ from functools import lru_cache
|
||||||
|
|
||||||
logger = get_logger("prompt")
|
logger = get_logger("prompt")
|
||||||
|
|
||||||
class Prompt():
|
_SCOPES = [
|
||||||
prompt_template_path : str #프롬프트 경로
|
"https://www.googleapis.com/auth/spreadsheets.readonly",
|
||||||
prompt_template : str # fstring 포맷
|
"https://www.googleapis.com/auth/drive.readonly"
|
||||||
prompt_model : str
|
]
|
||||||
|
|
||||||
prompt_input_class = BaseModel # pydantic class 자체를(instance 아님) 변수로 가짐
|
class Prompt():
|
||||||
|
sheet_name: str
|
||||||
|
prompt_template: str
|
||||||
|
prompt_model: str
|
||||||
|
|
||||||
|
prompt_input_class = BaseModel
|
||||||
prompt_output_class = BaseModel
|
prompt_output_class = BaseModel
|
||||||
|
|
||||||
def __init__(self, prompt_template_path, prompt_input_class, prompt_output_class, prompt_model):
|
def __init__(self, sheet_name, prompt_input_class, prompt_output_class):
|
||||||
self.prompt_template_path = prompt_template_path
|
self.sheet_name = sheet_name
|
||||||
self.prompt_input_class = prompt_input_class
|
self.prompt_input_class = prompt_input_class
|
||||||
self.prompt_output_class = prompt_output_class
|
self.prompt_output_class = prompt_output_class
|
||||||
self.prompt_template = self.read_prompt()
|
self.prompt_template, self.prompt_model = self._read_from_sheets()
|
||||||
self.prompt_model = prompt_model
|
|
||||||
|
def _read_from_sheets(self) -> tuple[str, str]:
|
||||||
|
creds = Credentials.from_service_account_file(
|
||||||
|
prompt_settings.GOOGLE_SERVICE_ACCOUNT_JSON, scopes=_SCOPES
|
||||||
|
)
|
||||||
|
gc = gspread.authorize(creds)
|
||||||
|
ws = gc.open_by_key(prompt_settings.PROMPT_SPREADSHEET).worksheet(self.sheet_name)
|
||||||
|
model = ws.cell(2, 2).value
|
||||||
|
input_text = ws.cell(3, 2).value
|
||||||
|
return input_text, model
|
||||||
|
|
||||||
def _reload_prompt(self):
|
def _reload_prompt(self):
|
||||||
self.prompt_template = self.read_prompt()
|
self.prompt_template, self.prompt_model = self._read_from_sheets()
|
||||||
|
|
||||||
def read_prompt(self) -> tuple[str, dict]:
|
|
||||||
with open(self.prompt_template_path, "r") as fp:
|
|
||||||
prompt_template = fp.read()
|
|
||||||
|
|
||||||
return prompt_template
|
|
||||||
|
|
||||||
def build_prompt(self, input_data:dict, silent:bool = False) -> str:
|
def build_prompt(self, input_data:dict, silent:bool = False) -> str:
|
||||||
verified_input = self.prompt_input_class(**input_data)
|
verified_input = self.prompt_input_class(**input_data)
|
||||||
|
|
@ -41,40 +50,36 @@ class Prompt():
|
||||||
return build_template
|
return build_template
|
||||||
|
|
||||||
marketing_prompt = Prompt(
|
marketing_prompt = Prompt(
|
||||||
prompt_template_path = os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.MARKETING_PROMPT_FILE_NAME),
|
sheet_name="marketing",
|
||||||
prompt_input_class = MarketingPromptInput,
|
prompt_input_class=MarketingPromptInput,
|
||||||
prompt_output_class = MarketingPromptOutput,
|
prompt_output_class=MarketingPromptOutput,
|
||||||
prompt_model = prompt_settings.MARKETING_PROMPT_MODEL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
lyric_prompt = Prompt(
|
lyric_prompt = Prompt(
|
||||||
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.LYRIC_PROMPT_FILE_NAME),
|
sheet_name="lyric",
|
||||||
prompt_input_class = LyricPromptInput,
|
prompt_input_class=LyricPromptInput,
|
||||||
prompt_output_class = LyricPromptOutput,
|
prompt_output_class=LyricPromptOutput,
|
||||||
prompt_model = prompt_settings.LYRIC_PROMPT_MODEL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yt_upload_prompt = Prompt(
|
yt_upload_prompt = Prompt(
|
||||||
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.YOUTUBE_PROMPT_FILE_NAME),
|
sheet_name="yt_upload",
|
||||||
prompt_input_class = YTUploadPromptInput,
|
prompt_input_class=YTUploadPromptInput,
|
||||||
prompt_output_class = YTUploadPromptOutput,
|
prompt_output_class=YTUploadPromptOutput,
|
||||||
prompt_model = prompt_settings.YOUTUBE_PROMPT_MODEL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
image_autotag_prompt = Prompt(
|
image_autotag_prompt = Prompt(
|
||||||
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.IMAGE_TAG_PROMPT_FILE_NAME),
|
sheet_name="image_tag",
|
||||||
prompt_input_class = ImageTagPromptInput,
|
prompt_input_class=ImageTagPromptInput,
|
||||||
prompt_output_class = ImageTagPromptOutput,
|
prompt_output_class=ImageTagPromptOutput,
|
||||||
prompt_model = prompt_settings.IMAGE_TAG_PROMPT_MODEL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@lru_cache()
|
@lru_cache()
|
||||||
def create_dynamic_subtitle_prompt(length : int) -> Prompt:
|
def create_dynamic_subtitle_prompt(length: int) -> Prompt:
|
||||||
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.SUBTITLE_PROMPT_FILE_NAME)
|
return Prompt(
|
||||||
prompt_input_class = SubtitlePromptInput
|
sheet_name="subtitle",
|
||||||
prompt_output_class = SubtitlePromptOutput[length]
|
prompt_input_class=SubtitlePromptInput,
|
||||||
prompt_model = prompt_settings.SUBTITLE_PROMPT_MODEL
|
prompt_output_class=SubtitlePromptOutput[length],
|
||||||
return Prompt(prompt_template_path, prompt_input_class, prompt_output_class, prompt_model)
|
)
|
||||||
|
|
||||||
|
|
||||||
def reload_all_prompt():
|
def reload_all_prompt():
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
|
|
||||||
[Role & Objective]
|
|
||||||
Act as a content marketing expert with strong domain knowledge in the Korean pension / stay-accommodation industry.
|
|
||||||
Your goal is to produce a Marketing Intelligence Report that will be shown to accommodation owners BEFORE any content is generated.
|
|
||||||
The report must clearly explain what makes the property sellable, marketable, and scalable through content.
|
|
||||||
|
|
||||||
[INPUT]
|
|
||||||
- Business Name: {customer_name}
|
|
||||||
- Region: {region}
|
|
||||||
- Region Details: {detail_region_info}
|
|
||||||
|
|
||||||
[Core Analysis Requirements]
|
|
||||||
Analyze the property based on:
|
|
||||||
Location, concept, and nearby environment
|
|
||||||
Target customer behavior and reservation decision factors
|
|
||||||
Include:
|
|
||||||
- Target customer segments & personas
|
|
||||||
- Unique Selling Propositions (USPs)
|
|
||||||
- Competitive landscape (direct & indirect competitors)
|
|
||||||
- Market positioning
|
|
||||||
|
|
||||||
[Key Selling Point Structuring – UI Optimized]
|
|
||||||
From the analysis above, extract the main Key Selling Points using the structure below.
|
|
||||||
Rules:
|
|
||||||
Focus only on factors that directly influence booking decisions
|
|
||||||
Each selling point must be concise and visually scannable
|
|
||||||
Language must be reusable for ads, short-form videos, and listing headlines
|
|
||||||
Avoid full sentences in descriptions; use short selling phrases
|
|
||||||
Do not provide in report
|
|
||||||
|
|
||||||
Output format:
|
|
||||||
[Category]
|
|
||||||
(Tag keyword – 5~8 words, noun-based, UI oval-style)
|
|
||||||
One-line selling phrase (not a full sentence)
|
|
||||||
Limit:
|
|
||||||
5 to 8 Key Selling Points only
|
|
||||||
Do not provide in report
|
|
||||||
|
|
||||||
[Content & Automation Readiness Check]
|
|
||||||
Ensure that:
|
|
||||||
Each tag keyword can directly map to a content theme
|
|
||||||
Each selling phrase can be used as:
|
|
||||||
- Video hook
|
|
||||||
- Image headline
|
|
||||||
- Ad copy snippet
|
|
||||||
|
|
||||||
|
|
||||||
[Tag Generation Rules]
|
|
||||||
- Tags must include **only core keywords that can be directly used for viral video song lyrics**
|
|
||||||
- Each tag should be selected with **search discovery + emotional resonance + reservation conversion** in mind
|
|
||||||
- The number of tags must be **exactly 5**
|
|
||||||
- Tags must be **nouns or short keyword phrases**; full sentences are strictly prohibited
|
|
||||||
- The following categories must be **balanced and all represented**:
|
|
||||||
1) **Location / Local context** (region name, neighborhood, travel context)
|
|
||||||
2) **Accommodation positioning** (emotional stay, private stay, boutique stay, etc.)
|
|
||||||
3) **Emotion / Experience** (healing, rest, one-day escape, memory, etc.)
|
|
||||||
4) **SNS / Viral signals** (Instagram vibes, picture-perfect day, aesthetic travel, etc.)
|
|
||||||
5) **Travel & booking intent** (travel, getaway, stay, relaxation, etc.)
|
|
||||||
|
|
||||||
- If a brand name exists, **at least one tag must include the brand name or a brand-specific expression**
|
|
||||||
- Avoid overly generic keywords (e.g., “hotel”, “travel” alone); **prioritize distinctive, differentiating phrases**
|
|
||||||
- The final output must strictly follow the JSON format below, with no additional text
|
|
||||||
|
|
||||||
"tags": ["Tag1", "Tag2", "Tag3", "Tag4", "Tag5"]
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
You are an expert image analyst and content tagger.
|
|
||||||
|
|
||||||
Analyze the provided image carefully and select the most appropriate tags for each category below.
|
|
||||||
and lastly, check this image is acceptable for marketing advertisement video.
|
|
||||||
|
|
||||||
and set NarrativePreference score for each narrative phase.
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
- For each category, select between 0 and the specified maximum number of tags.
|
|
||||||
- Only select tags from the exact list provided for each category. Do NOT invent or modify tags.
|
|
||||||
- If no tags in a category are applicable, return an empty list for that category.
|
|
||||||
- Base your selections solely on what is visually present or strongly implied in the image.
|
|
||||||
-
|
|
||||||
|
|
||||||
## Tag Categories
|
|
||||||
space_type:{space_type}
|
|
||||||
subject: {subject}
|
|
||||||
camera:{camera}
|
|
||||||
motion_recommended: {motion_recommended}
|
|
||||||
|
|
||||||
## Output
|
|
||||||
Return a JSON object where each key is a category name and the value is a list of selected tags.
|
|
||||||
Selected tags must be chosen from the available tags of that category only.
|
|
||||||
and NarrativePreference score for each narrative phase.
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
|
|
||||||
[ROLE]
|
|
||||||
You are a content marketing expert, brand strategist, and creative songwriter
|
|
||||||
specializing in Korean pension / accommodation businesses.
|
|
||||||
You create lyrics strictly based on Brand & Marketing Intelligence analysis
|
|
||||||
and optimized for viral short-form video content.
|
|
||||||
Marketing Intelligence Report is background reference.
|
|
||||||
|
|
||||||
[INPUT]
|
|
||||||
Business Name: {customer_name}
|
|
||||||
Region: {region}
|
|
||||||
Region Details: {detail_region_info}
|
|
||||||
Brand & Marketing Intelligence Report: {marketing_intelligence_summary}
|
|
||||||
Output Language: {language}
|
|
||||||
|
|
||||||
[INTERNAL ANALYSIS – DO NOT OUTPUT]
|
|
||||||
Internally analyze the following to guide all creative decisions:
|
|
||||||
- Core brand identity and positioning
|
|
||||||
- Emotional hooks derived from selling points
|
|
||||||
- Target audience lifestyle, desires, and travel motivation
|
|
||||||
- Regional atmosphere and symbolic imagery
|
|
||||||
- How the stay converts into “shareable moments”
|
|
||||||
- Which selling points must surface implicitly in lyrics
|
|
||||||
|
|
||||||
[LYRICS & MUSIC CREATION TASK]
|
|
||||||
Based on the Brand & Marketing Intelligence Report for [{customer_name} ({region})], generate:
|
|
||||||
- Original promotional lyrics
|
|
||||||
- Music attributes for AI music generation (Suno-compatible prompt)
|
|
||||||
The output must be designed for VIRAL DIGITAL CONTENT
|
|
||||||
(short-form video, reels, ads).
|
|
||||||
|
|
||||||
[LYRICS REQUIREMENTS]
|
|
||||||
Mandatory Inclusions:
|
|
||||||
- Business name
|
|
||||||
- Region name
|
|
||||||
- Promotion subject
|
|
||||||
- Promotional expressions including:
|
|
||||||
{promotional_expression_example}
|
|
||||||
|
|
||||||
Content Rules:
|
|
||||||
- Lyrics must be emotionally driven, not descriptive listings
|
|
||||||
- Selling points must be IMPLIED, not explained
|
|
||||||
- Must sound natural when sung
|
|
||||||
- Must feel like a lifestyle moment, not an advertisement
|
|
||||||
|
|
||||||
Tone & Style:
|
|
||||||
- Warm, emotional, and aspirational
|
|
||||||
- Trendy, viral-friendly phrasing
|
|
||||||
- Calm but memorable hooks
|
|
||||||
- Suitable for travel / stay-related content
|
|
||||||
|
|
||||||
[SONG & MUSIC ATTRIBUTES – FOR SUNO PROMPT]
|
|
||||||
After the lyrics, generate a concise music prompt including:
|
|
||||||
Song mood (emotional keywords)
|
|
||||||
BPM range
|
|
||||||
Recommended genres (max 2)
|
|
||||||
Key musical motifs or instruments
|
|
||||||
Overall vibe (1 short sentence)
|
|
||||||
|
|
||||||
[CRITICAL LANGUAGE REQUIREMENT – ABSOLUTE RULE]
|
|
||||||
ALL OUTPUT MUST BE 100% WRITTEN IN {language}.
|
|
||||||
no mixed languages
|
|
||||||
All names, places, and expressions must be in {language}
|
|
||||||
Any violation invalidates the entire output
|
|
||||||
|
|
||||||
[OUTPUT RULES – STRICT]
|
|
||||||
{timing_rules}
|
|
||||||
|
|
||||||
No explanations
|
|
||||||
No headings
|
|
||||||
No bullet points
|
|
||||||
No analysis
|
|
||||||
No extra text
|
|
||||||
|
|
||||||
[FAILURE FORMAT]
|
|
||||||
If generation is impossible:
|
|
||||||
ERROR: Brief reason in English
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
# Role
|
|
||||||
Act as a Senior Brand Strategist and Marketing Data Analyst. Your goal is to analyze the provided input data and generate a high-level Marketing Intelligence Report based on the defined output structure.
|
|
||||||
|
|
||||||
# Input Data
|
|
||||||
* **Customer Name:** {customer_name}
|
|
||||||
* **Region:** {region}
|
|
||||||
* **Detail Region Info:** {detail_region_info}
|
|
||||||
|
|
||||||
# Output Rules
|
|
||||||
1. **Language:** All descriptive content must be written in **Korean (한국어)**.
|
|
||||||
2. **Terminology:** Use professional marketing terminology suitable for the hospitality and stay industry.
|
|
||||||
3. **Strict Selection for `selling_points.english_category` and `selling_points.korean_category`:** You must select the value for both category field in `selling_points` strictly from the following English - Korean set allowed list to ensure UI compatibility:
|
|
||||||
* `LOCATION` (입지 환경), `CONCEPT` (브랜드 컨셉), `PRIVACY` (프라이버시), `NIGHT MOOD` (야간 감성), `HEALING` (힐링 요소), `PHOTO SPOT` (포토 스팟), `SHORT GETAWAY` (숏브레이크), `HOSPITALITY` (서비스), `SWIMMING POOL` (수영장), `JACUZZI` (자쿠지), `BBQ PARTY` (바베큐), `FIRE PIT` (불멍), `GARDEN` (정원), `BREAKFAST` (조식), `KIDS FRIENDLY` (키즈 케어), `PET FRIENDLY` (애견 동반), `OCEAN VIEW` (오션뷰), `PRIVATE POOL` (개별 수영장), `OCEAN VIEW`, `PRIVATE POOL`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Instruction per Output Field (Mapping Logic)
|
|
||||||
|
|
||||||
### 1. brand_identity
|
|
||||||
* **`location_feature_analysis`**: Analyze the marketing advantages of the given `{region}` and `{detail_region_info}`. Explain why this specific location is attractive to travelers. summarize in 1-2 sentences. (e.g., proximity to nature, accessibility from Seoul, or unique local atmosphere).
|
|
||||||
* **`concept_scalability`**: Based on `{customer_name}`, analyze how the brand's core concept can expand into a total customer experience or additional services. summarize in 1-2 sentences.
|
|
||||||
|
|
||||||
### 2. market_positioning
|
|
||||||
* **`category_definition`**: Define a sharp, niche market category for this business (e.g., "Private Forest Cabin" or "Luxury Kids Pool Villa").
|
|
||||||
* **`core_value`**: Identify the single most compelling emotional or functional value that distinguishes `{customer_name}` from competitors.
|
|
||||||
|
|
||||||
### 3. target_persona
|
|
||||||
Generate a list of personas based on the following:
|
|
||||||
* **`persona`**: Provide a descriptive name and profile for the target group. Must be **20 characters or fewer**.
|
|
||||||
* **`age`**: Set `min_age` and `max_age` (Integer 0-100) that accurately reflects the segment.
|
|
||||||
* **`favor_target`**: List specific elements or vibes this persona prefers (e.g., "Minimalist interior", "Pet-friendly facilities").
|
|
||||||
* **`decision_trigger`**: Identify the specific "Hook" or facility that leads this persona to finalize a booking.
|
|
||||||
|
|
||||||
### 4. selling_points
|
|
||||||
Generate 5-8 selling points:
|
|
||||||
* **`english_category`**: Strictly use one keyword from the English allowed list provided in the Output Rules.
|
|
||||||
* **`korean category`**: Strictly use one keyword from the Korean allowed list provided in the Output Rules . It must be matched with english category.
|
|
||||||
* **`description`**: A short, punchy marketing phrase in Korean (15~30 characters).
|
|
||||||
* **`score`**: An integer (0-100) representing the strength of this feature based on the brand's potential.
|
|
||||||
|
|
||||||
### 5. target_keywords
|
|
||||||
* **`target_keywords`**: Provide a list of 10 highly relevant marketing keywords or hashtags for search engine optimization and social media targeting. Do not insert # in front of hashtag.
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
[ROLE]
|
|
||||||
You are a YouTube SEO/AEO content strategist specialized in local stay, pension, and accommodation brands in Korea.
|
|
||||||
You create search-optimized, emotionally appealing, and action-driving titles and descriptions based on Brand & Marketing Intelligence.
|
|
||||||
|
|
||||||
Your goal is to:
|
|
||||||
|
|
||||||
Increase search visibility
|
|
||||||
Improve click-through rate
|
|
||||||
Reflect the brand’s positioning
|
|
||||||
Trigger emotional interest
|
|
||||||
Encourage booking or inquiry actions through subtle CTA
|
|
||||||
|
|
||||||
|
|
||||||
[INPUT]
|
|
||||||
Business Name: {customer_name}
|
|
||||||
Region Details: {detail_region_info}
|
|
||||||
Brand & Marketing Intelligence Report: {marketing_intelligence_summary}
|
|
||||||
Target Keywords: {target_keywords}
|
|
||||||
Output Language: {language}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[INTERNAL ANALYSIS – DO NOT OUTPUT]
|
|
||||||
Analyze the following from the marketing intelligence:
|
|
||||||
|
|
||||||
Core brand concept
|
|
||||||
Main emotional promise
|
|
||||||
Primary target persona
|
|
||||||
Top 2–3 USP signals
|
|
||||||
Stay context (date, healing, local trip, etc.)
|
|
||||||
Search intent behind the target keywords
|
|
||||||
Main booking trigger
|
|
||||||
Emotional moment that would make the viewer want to stay
|
|
||||||
Use these to guide:
|
|
||||||
|
|
||||||
Title tone
|
|
||||||
Opening CTA line
|
|
||||||
Emotional hook in the first sentences
|
|
||||||
|
|
||||||
|
|
||||||
[TITLE GENERATION RULES]
|
|
||||||
|
|
||||||
The title must:
|
|
||||||
|
|
||||||
Include the business name or region when natural
|
|
||||||
Always wrap the business name in quotation marks
|
|
||||||
Example: “스테이 머뭄”
|
|
||||||
Include 1–2 high-intent keywords
|
|
||||||
Reflect emotional positioning
|
|
||||||
Suggest a desirable stay moment
|
|
||||||
Sound like a natural YouTube title, not an advertisement
|
|
||||||
Length rules:
|
|
||||||
|
|
||||||
Hard limit: 100 characters
|
|
||||||
Target range: 45–65 characters
|
|
||||||
Place primary keyword in the first half
|
|
||||||
Avoid:
|
|
||||||
|
|
||||||
ALL CAPS
|
|
||||||
Excessive symbols
|
|
||||||
Price or promotion language
|
|
||||||
Hard-sell expressions
|
|
||||||
|
|
||||||
|
|
||||||
[DESCRIPTION GENERATION RULES]
|
|
||||||
|
|
||||||
Character rules:
|
|
||||||
|
|
||||||
Maximum length: 1,000 characters
|
|
||||||
Critical information must appear within the first 150 characters
|
|
||||||
Language style rules (mandatory):
|
|
||||||
|
|
||||||
Use polite Korean honorific style
|
|
||||||
Replace “있나요?” with “있으신가요?”
|
|
||||||
Do not start sentences with “이곳은”
|
|
||||||
Replace “선택이 됩니다” with “추천 드립니다”
|
|
||||||
Always wrap the business name in quotation marks
|
|
||||||
Example: “스테이 머뭄”
|
|
||||||
Avoid vague location words like “근대거리” alone
|
|
||||||
Use specific phrasing such as:
|
|
||||||
“군산 근대역사문화거리 일대”
|
|
||||||
Structure:
|
|
||||||
|
|
||||||
Opening CTA (first line)
|
|
||||||
Must be a question or gentle suggestion
|
|
||||||
Must use honorific tone
|
|
||||||
Example:
|
|
||||||
“조용히 쉴 수 있는 군산숙소를 찾고 있으신가요?”
|
|
||||||
Core Stay Introduction (within first 150 characters total)
|
|
||||||
Mention business name with quotation marks
|
|
||||||
Mention region
|
|
||||||
Include main keyword
|
|
||||||
Briefly describe the stay experience
|
|
||||||
Brand Experience
|
|
||||||
Core value and emotional promise
|
|
||||||
Based on marketing intelligence positioning
|
|
||||||
Key Highlights (3–4 short lines)
|
|
||||||
Derived from USP signals
|
|
||||||
Natural sentences
|
|
||||||
Focus on booking-trigger moments
|
|
||||||
Local Context
|
|
||||||
Mention nearby experiences
|
|
||||||
Use specific local references
|
|
||||||
Example:
|
|
||||||
“군산 근대역사문화거리 일대 산책이나 로컬 카페 투어”
|
|
||||||
Soft Closing Line
|
|
||||||
One gentle, non-salesy closing sentence
|
|
||||||
Must end with a recommendation tone
|
|
||||||
Example:
|
|
||||||
“군산에서 조용한 시간을 보내고 싶다면 ‘스테이 머뭄’을 추천 드립니다.”
|
|
||||||
|
|
||||||
|
|
||||||
[SEO & AEO RULES]
|
|
||||||
|
|
||||||
Naturally integrate 3–5 keywords from {target_keywords}
|
|
||||||
Avoid keyword stuffing
|
|
||||||
Use conversational, search-like phrasing
|
|
||||||
Optimize for:
|
|
||||||
YouTube search
|
|
||||||
Google video results
|
|
||||||
AI answer summaries
|
|
||||||
Keywords should appear in:
|
|
||||||
|
|
||||||
Title (1–2)
|
|
||||||
First 150 characters of description
|
|
||||||
Highlight or context sections
|
|
||||||
|
|
||||||
|
|
||||||
[LANGUAGE RULE]
|
|
||||||
|
|
||||||
All output must be written entirely in {language}.
|
|
||||||
No mixed languages.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[OUTPUT FORMAT – STRICT]
|
|
||||||
|
|
||||||
title:
|
|
||||||
description:
|
|
||||||
|
|
||||||
No explanations.
|
|
||||||
No headings.
|
|
||||||
No extra text.
|
|
||||||
18
config.py
18
config.py
|
|
@ -181,22 +181,8 @@ class CreatomateSettings(BaseSettings):
|
||||||
model_config = _base_config
|
model_config = _base_config
|
||||||
|
|
||||||
class PromptSettings(BaseSettings):
|
class PromptSettings(BaseSettings):
|
||||||
PROMPT_FOLDER_ROOT : str = Field(default="./app/utils/prompts/templates")
|
GOOGLE_SERVICE_ACCOUNT_JSON: str = Field(...)
|
||||||
|
PROMPT_SPREADSHEET: str = Field(...)
|
||||||
MARKETING_PROMPT_FILE_NAME : str = Field(default="marketing_prompt.txt")
|
|
||||||
MARKETING_PROMPT_MODEL : str = Field(default="gpt-5.2")
|
|
||||||
|
|
||||||
LYRIC_PROMPT_FILE_NAME : str = Field(default="lyric_prompt.txt")
|
|
||||||
LYRIC_PROMPT_MODEL : str = Field(default="gpt-5-mini")
|
|
||||||
|
|
||||||
YOUTUBE_PROMPT_FILE_NAME : str = Field(default="yt_upload_prompt.txt")
|
|
||||||
YOUTUBE_PROMPT_MODEL : str = Field(default="gpt-5-mini")
|
|
||||||
|
|
||||||
IMAGE_TAG_PROMPT_FILE_NAME : str = Field(...)
|
|
||||||
IMAGE_TAG_PROMPT_MODEL : str = Field(...)
|
|
||||||
|
|
||||||
SUBTITLE_PROMPT_FILE_NAME : str = Field(...)
|
|
||||||
SUBTITLE_PROMPT_MODEL : str = Field(...)
|
|
||||||
|
|
||||||
model_config = _base_config
|
model_config = _base_config
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ dependencies = [
|
||||||
"beautifulsoup4>=4.14.3",
|
"beautifulsoup4>=4.14.3",
|
||||||
"fastapi-cli>=0.0.16",
|
"fastapi-cli>=0.0.16",
|
||||||
"fastapi[standard]>=0.125.0",
|
"fastapi[standard]>=0.125.0",
|
||||||
|
"gspread>=6.2.1",
|
||||||
"openai>=2.13.0",
|
"openai>=2.13.0",
|
||||||
"playwright>=1.57.0",
|
"playwright>=1.57.0",
|
||||||
"pydantic-settings>=2.12.0",
|
"pydantic-settings>=2.12.0",
|
||||||
|
|
|
||||||
115
uv.lock
115
uv.lock
|
|
@ -178,6 +178,31 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.3.1"
|
version = "8.3.1"
|
||||||
|
|
@ -413,6 +438,32 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
|
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-auth"
|
||||||
|
version = "2.49.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
{ name = "pyasn1-modules" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-auth-oauthlib"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "google-auth" },
|
||||||
|
{ name = "requests-oauthlib" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a6/82/62482931dcbe5266a2680d0da17096f2aab983ecb320277d9556700ce00e/google_auth_oauthlib-1.3.1.tar.gz", hash = "sha256:14c22c7b3dd3d06dbe44264144409039465effdd1eef94f7ce3710e486cc4bfa", size = 21663, upload-time = "2026-03-30T22:49:56.408Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/e0/cb454a95f460903e39f101e950038ec24a072ca69d0a294a6df625cc1627/google_auth_oauthlib-1.3.1-py3-none-any.whl", hash = "sha256:1a139ef23f1318756805b0e95f655c238bffd29655329a2978218248da4ee7f8", size = 19247, upload-time = "2026-03-30T20:02:23.894Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "3.3.0"
|
version = "3.3.0"
|
||||||
|
|
@ -429,6 +480,19 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" },
|
{ url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gspread"
|
||||||
|
version = "6.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "google-auth" },
|
||||||
|
{ name = "google-auth-oauthlib" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/91/83/42d1d813822ed016d77aabadc99b09de3b5bd68532fd6bae23fd62347c41/gspread-6.2.1.tar.gz", hash = "sha256:2c7c99f7c32ebea6ec0d36f2d5cbe8a2be5e8f2a48bde87ad1ea203eff32bd03", size = 82590, upload-time = "2025-05-14T15:56:25.254Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/76/563fb20dedd0e12794d9a12cfe0198458cc0501fdc7b034eee2166d035d5/gspread-6.2.1-py3-none-any.whl", hash = "sha256:6d4ec9f1c23ae3c704a9219026dac01f2b328ac70b96f1495055d453c4c184db", size = 59977, upload-time = "2025-05-14T15:56:24.014Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
|
@ -654,6 +718,7 @@ dependencies = [
|
||||||
{ name = "beautifulsoup4" },
|
{ name = "beautifulsoup4" },
|
||||||
{ name = "fastapi", extra = ["standard"] },
|
{ name = "fastapi", extra = ["standard"] },
|
||||||
{ name = "fastapi-cli" },
|
{ name = "fastapi-cli" },
|
||||||
|
{ name = "gspread" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
{ name = "playwright" },
|
{ name = "playwright" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
|
|
@ -683,6 +748,7 @@ requires-dist = [
|
||||||
{ name = "beautifulsoup4", specifier = ">=4.14.3" },
|
{ name = "beautifulsoup4", specifier = ">=4.14.3" },
|
||||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.125.0" },
|
{ name = "fastapi", extras = ["standard"], specifier = ">=0.125.0" },
|
||||||
{ name = "fastapi-cli", specifier = ">=0.0.16" },
|
{ name = "fastapi-cli", specifier = ">=0.0.16" },
|
||||||
|
{ name = "gspread", specifier = ">=6.2.1" },
|
||||||
{ name = "openai", specifier = ">=2.13.0" },
|
{ name = "openai", specifier = ">=2.13.0" },
|
||||||
{ name = "playwright", specifier = ">=1.57.0" },
|
{ name = "playwright", specifier = ">=1.57.0" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.12.0" },
|
{ name = "pydantic-settings", specifier = ">=2.12.0" },
|
||||||
|
|
@ -703,6 +769,15 @@ dev = [
|
||||||
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
|
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oauthlib"
|
||||||
|
version = "3.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "2.15.0"
|
version = "2.15.0"
|
||||||
|
|
@ -807,6 +882,18 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" },
|
{ url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyasn1-modules"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyasn1" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "3.0"
|
version = "3.0"
|
||||||
|
|
@ -1010,6 +1097,34 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.33.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests-oauthlib"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "oauthlib" },
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "14.2.0"
|
version = "14.2.0"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue