kuoe commited on
Commit
101908c
ยท
verified ยท
1 Parent(s): 84b7c6c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +182 -28
app.py CHANGED
@@ -14,32 +14,118 @@ try:
14
  except Exception:
15
  HAS_PYTRENDS = False
16
 
17
- app = FastAPI(title="NocoAI Title Forge (Shorts Optimized, Localized + Topic Forge)")
18
 
19
  # ===== ๋ชจ๋ธ =====
20
  MODEL_ID = "google/mt5-small"
21
  pipe = pipeline("text2text-generation", model=MODEL_ID)
22
 
23
- # ===== ์ž๋™ ๋ชจ๋“œ ์‹œ๋“œ =====
24
  SEED_TOPICS = [
25
- "์Šค๋งˆํŠธํฐ ๋น„๊ต: ๊ฐค๋Ÿญ์‹œ vs ์•„์ดํฐ ์ตœ์‹  ๋ฐˆ",
26
- "AI๋กœ 1๋ถ„ ๋งŒ์— ์˜์ƒ ๋งŒ๋“œ๋Š” ๋ฒ•",
27
- "์š”์ฆ˜ ์œ ํ–‰ ๋ฏธ๋‹ˆ๋ฉ€ ์ธํ…Œ๋ฆฌ์–ด ์•„์ดํ…œ",
28
- "ํŽธ์˜์  ๊ฟ€์กฐํ•ฉ TOP5",
29
- "์›” 10๋งŒ์› ์ ˆ์•ฝ ๋ฃจํ‹ด",
30
- "๊ณ ์ธ๋ฌผ๋งŒ ์•„๋Š” ๊ฒŒ์ž„ ๊ฟ€ํŒ",
31
- "K-ํŒ ์ฑŒ๋ฆฐ์ง€ ๋ฐˆ",
32
- "ํ‡ด๊ทผ ํ›„ 30๋ถ„ ์šด๋™ ์ฑŒ๋ฆฐ์ง€",
33
- "๋”ฅํŽ˜์ดํฌ ๋…ผ๋ž€, ์–ด๋””๊นŒ์ง€ ํ—ˆ์šฉ?",
34
- "ํ•œ ๋‹ฌ ๋งŒ์— ์˜์–ด ๋ฆฌ์Šค๋‹ ์˜ฌ๋ฆฌ๋Š” ๋ฒ•",
35
- "์•„์ดํŒจ๋“œ ์ƒ์‚ฐ์„ฑ ์„ธํŒ…",
36
- "ํŠน์ด์  ์˜จ AI ๋ฐˆ ๋ชจ์Œ",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  ]
38
  _seed_iter = itertools.cycle(SEED_TOPICS)
39
 
40
  # ===== ์š”์ฒญ/์‘๋‹ต =====
41
  class GenerateReq(BaseModel):
42
- mode: Optional[str] = Field(None, description="auto | topic (์ง€์ • ์•ˆ ํ•ด๋„ ๋จ)")
43
  topic: Optional[str] = None
44
  count: int = Field(30, ge=1, le=80, description="์ƒ์„ฑํ•  ์ œ๋ชฉ ์ˆ˜(์ตœ๋Œ€ 80)")
45
  # ์–ธ์–ด/๊ตญ๊ฐ€ (ํ˜„์ง€ํ™”)
@@ -65,6 +151,25 @@ class TopicResp(BaseModel):
65
  country: str
66
  category: Optional[str] = None
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  # ===== ์–ธ์–ด/๊ตญ๊ฐ€ ์ •๊ทœํ™” =====
69
  LANG_MAP = {
70
  "ko": ("ko", "ํ•œ๊ตญ์–ด"), "ko-kr": ("ko", "ํ•œ๊ตญ์–ด"), "korean": ("ko", "ํ•œ๊ตญ์–ด"), "ํ•œ๊ตญ์–ด": ("ko", "ํ•œ๊ตญ์–ด"),
@@ -73,8 +178,6 @@ LANG_MAP = {
73
  "es": ("es", "Espaรฑol"), "es-es": ("es", "Espaรฑol"), "es-mx": ("es", "Espaรฑol"), "spanish": ("es", "Espaรฑol"), "์ŠคํŽ˜์ธ์–ด": ("es", "Espaรฑol"),
74
  "zh": ("zh", "ไธญๆ–‡"), "zh-cn": ("zh", "ไธญๆ–‡"), "chinese": ("zh", "ไธญๆ–‡"), "์ค‘๊ตญ์–ด": ("zh", "ไธญๆ–‡"), "ไธญๆ–‡": ("zh", "ไธญๆ–‡"),
75
  }
76
-
77
- # pytrends ๊ตญ๊ฐ€ ํ‚ค (pn, locale)
78
  PNT_MAP = {
79
  "KR": ("south_korea", "ko-KR"),
80
  "US": ("united_states", "en-US"),
@@ -83,7 +186,6 @@ PNT_MAP = {
83
  "MX": ("mexico", "es-419"),
84
  "CN": ("china", "zh-CN"), # pytrends ๋ฏธ์ง€์›/๋ถˆ์•ˆ์ • ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ
85
  }
86
-
87
  LANG_DEFAULT_COUNTRY = {"ko": "KR", "en": "US", "ja": "JP", "es": "ES", "zh": "CN"}
88
 
89
  def normalize_lang_and_country(lang_in: str, country_in: Optional[str]) -> Tuple[str, str, str]:
@@ -117,7 +219,6 @@ EXAMPLES = {
117
  "AIไธ€ๅˆ†้’Ÿๅš็ˆ†ๆฌพ็Ÿญ่ง†้ข‘๏ผŒ็œŸ็š„ๅ‡็š„๏ผŸ\n"
118
  "ไพฟๅˆฉๅบ—็ฅž็ป„ๅˆTOP5๏ผŒๆœ€ๅŽไธ€ไธช็›ดๆŽฅ็‚ธ่ฃ‚๏ผ\n"),
119
  }
120
-
121
  HOOKS = {
122
  "ko": ["์ด๊ฑด ๋ชฐ๋ž์ฃ ?", "์†Œ๋ฆ„", "์ถฉ๊ฒฉ", "์‹คํ™”๋ƒ", "TOP5", "ใ„นใ…‡", "๋Œ€๋ฐ•", "๋ฏธ์ณค๋‹ค", "๋ฐ˜์ „ ์žˆ์Œ", "ํ•œ์ค„ ์š”์•ฝ", "์ง€๊ธˆ ์•ˆ ๋ณด๋ฉด ์†ํ•ด", "30์ดˆ ์ปท", "๋๊นŒ์ง€ ๋ณด์„ธ์š”", "์•Œ๊ณ  ๋ณด๋ฉด ๋” ์†Œ๋ฆ„", "์ดˆ๊ฐ„๋‹จ"],
123
  "en": ["did you know?", "shocking", "insane", "is this real?", "TOP 5", "no way", "viral", "mind-blown", "plot twist", "one-line tip", "donโ€™t miss this", "30s hack", "watch till the end", "low-key genius"],
@@ -125,7 +226,6 @@ HOOKS = {
125
  "es": ["ยฟlo sabรญas?", "impactante", "una locura", "ยฟes real?", "TOP 5", "no puede ser", "viral", "mente en blanco", "giro inesperado", "tip en una lรญnea", "no te lo pierdas", "truco de 30s", "hasta el final", "dato clave"],
126
  "zh": ["้œ‡ๆƒŠ", "ๅคช็ฆป่ฐฑไบ†ๅง", "ไฝ ็ปๅฏนๆƒณไธๅˆฐ", "TOP5", "็ฝ‘ๅ‹้ƒฝๆƒŠๅ‘†ไบ†", "ๅคช็‚ธไบ†", "็ฌ‘ๆญป", "็ ด้˜ฒไบ†", "่ฟ™ๆ“ไฝœ้€†ๅคฉ", "็œŸๆ•ขๆ‹", "ไธ็œ‹ๅŽๆ‚”", "็ป†ๆ€ๆžๆ", "็ปไบ†", "ๅคช็‹ ไบ†"],
127
  }
128
-
129
  TAILS = {
130
  "ko": ["๋ฐฉ๋ฒ• ๊ณต๊ฐœ", "์‹œ์ฒญ์ž ํ›„๊ธฐ ํ„ฐ์ง", "์ „/ํ›„ ๋น„๊ต", "์ดˆ๋ณด๋„ ๊ฐ€๋Šฅ", "์ˆจ์€ ๊ธฐ๋Šฅ", "๋ฐ˜์ „ ์ฃผ์˜", "์‹ค์ „ ๊ฟ€ํŒ", "๋ชจ๋ฅด๋ฉด ์†ํ•ด", "ํ•œ ๋ฒˆ์— ๋", "๋ฐ”๋กœ ์จ๋จน๋Š” ์Šคํ‚ฌ", "ํ•ต์‹ฌ๋งŒ ์š”์•ฝ", "์ •๋ฆฌํ•ด๋“œ๋ฆผ"],
131
  "en": ["method revealed", "viewers went crazy", "before vs after", "beginner friendly", "hidden feature", "twist ahead", "pro tips", "donโ€™t miss out", "done in one go", "instant skill", "key points only", "quick recap"],
@@ -133,7 +233,6 @@ TAILS = {
133
  "es": ["mรฉtodo revelado", "los viewers enloquecen", "antes vs despuรฉs", "para principiantes", "funciรณn oculta", "plot twist", "tips pro", "no te lo pierdas", "todo de una", "skill al instante", "puntos clave", "resumen rรกpido"],
134
  "zh": ["ๅฎŒๆ•ด็‰ˆๆฅไบ†", "็ฝ‘ๅ‹็ƒญ่ฎฎไธญ", "็œ‹ๅฎŒๆˆ‘ๆฒ‰้ป˜ไบ†", "ๅ‰ๅŽๅฏนๆฏ”็ฆป่ฐฑ", "ๆ–ฐๆ‰‹ไนŸ่ƒฝๅš", "้š่—ๅŠŸ่ƒฝๆ›ๅ…‰", "่ƒŒๅŽๅŽŸๅ› ้œ‡ๆ’ผ", "ไธ็œ‹็œŸ็š„ไบ", "ๆ•™็จ‹ๆฅไบ†", "ๅ…จ็ฝ‘็ˆ†็ซ", "ไบฒๆต‹ๆœ‰ๆ•ˆ", "ๅนฒ่ดงๆ€ป็ป“"],
135
  }
136
-
137
  PROMPT_HEADER = {
138
  "ko": "์—ญํ• : ๋‹น์‹ ์€ ํ•ด๋‹น ๊ตญ๊ฐ€์˜ ์œ ํŠœ๋ธŒ ์‡ผ์ธ  ์นดํ”ผ๋ผ์ดํ„ฐ๋‹ค.\n๊ณผ์—…: ์•„๋ž˜ โ€˜์ฃผ์ œโ€™๋กœ ํ˜„์ง€ ์‹œ์ฒญ์ž๊ฐ€ ๋งŽ์ด ๋ณด๊ฒŒ๋” ๋ฐ”์ด๋Ÿด ์ œ๋ชฉ์„ ์ƒ์„ฑํ•˜๋ผ.\n",
139
  "en": "Role: You are a YouTube Shorts copywriter for the target country.\nTask: Generate viral titles so local viewers want to watch.\n",
@@ -236,22 +335,18 @@ _LEAK_PATTERNS = [
236
  r'^\s*[-โ€ข]\s*$',
237
  r'^\s*๊ฐ ์ œ๋ชฉ์€',
238
  ]
239
-
240
  def _fingerprint(s: str) -> str:
241
  return re.sub(r"[\s\W]+", "", s.lower())
242
-
243
  def _valid_len(s: str, lang_code: str) -> bool:
244
  if lang_code in ("en", "es"):
245
  words = re.findall(r"\w+", s)
246
  return 3 <= len(words) <= 14
247
  return 6 <= len(s) <= 36 # ko/ja/zh
248
-
249
  def _leak(s: str) -> bool:
250
  for p in _LEAK_PATTERNS:
251
  if re.search(p, s, flags=re.I):
252
  return True
253
  return False
254
-
255
  def postprocess(raw: str, lang_code: str, numberless: bool = True) -> List[str]:
256
  if not raw: return []
257
  raw = re.sub(r"<extra_id_\d+>", "", raw)
@@ -317,7 +412,6 @@ def generate_titles(topic: str, count: int, req, lang_code: str, pretty_lang: st
317
 
318
  # ===== ํ† ํ”ฝ ํฌ์ง€(์˜์ƒํ™” ๊ฐ€๋Šฅํ•œ ์ฃผ์ œ ์ž๋™ ์ƒ์„ฑ) =====
319
  TOPIC_CATEGORIES = ("ai-slice","ai-teach","ai-transform","ai-battle","ai-micro","mixed")
320
-
321
  OBJ = {
322
  "ko": {
323
  "fruit": ["์ˆ˜๋ฐ•","๋ง๊ณ ","ํŒŒ์ธ์• ํ”Œ","์‚ฌ๊ณผ","ํ‚ค์œ„","์šฉ๊ณผ","๋ณต์ˆญ์•„","๋ฐ”๋‚˜๋‚˜","์ฝ”์ฝ”๋„›","ํฌ๋„"],
@@ -342,7 +436,6 @@ OBJ = {
342
  }
343
  def _obj(lang: str) -> dict:
344
  return OBJ.get(lang, OBJ["ko"])
345
-
346
  def forge_topic(lang_code: str, category: str) -> str:
347
  o = _obj(lang_code)
348
  if category == "ai-slice":
@@ -366,6 +459,44 @@ def forge_topic(lang_code: str, category: str) -> str:
366
  return f"AI ๋งˆ์ดํฌ๋กœ ์„ธ๊ณ„ ํƒํ—˜: {t} ์† {m} ํ™•๋Œ€ ๊ด€์ฐฐ"
367
  return forge_topic(lang_code, random.choice(TOPIC_CATEGORIES[:-1])) # mixed
368
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  @app.get("/topic/auto", response_model=TopicResp)
370
  def topic_auto(
371
  count: int = Query(10, ge=1, le=100),
@@ -393,7 +524,7 @@ def topic_auto(
393
 
394
  @app.get("/")
395
  def root():
396
- return JSONResponse({"message": "NocoAI Title Forge (Shorts Optimized, Localized + Topic Forge). Try GET /topic/auto, POST /generate, or GET /generate/auto"}, media_type="application/json; charset=utf-8")
397
 
398
  @app.get("/health")
399
  def health():
@@ -437,3 +568,26 @@ def generate_auto(
437
  titles, used_prompt = generate_titles(topic, count, req, lang_code, pretty_lang)
438
  used_source = trend_source if (trend_source == "pytrends" and HAS_PYTRENDS) else "seed"
439
  return JSONResponse({"topic": topic, "titles": titles, "used_prompt": used_prompt, "trend_source": used_source}, media_type="application/json; charset=utf-8")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  except Exception:
15
  HAS_PYTRENDS = False
16
 
17
+ app = FastAPI(title="NocoAI Title & Prompt Forge (Shorts Optimized)")
18
 
19
  # ===== ๋ชจ๋ธ =====
20
  MODEL_ID = "google/mt5-small"
21
  pipe = pipeline("text2text-generation", model=MODEL_ID)
22
 
23
+ # ===== ์ตœ์‹  ํŠธ๋ Œ๋“œํ˜• ์‹œ๋“œ (AI ์ˆํผ ์นœํ™” 80+๊ฐœ) =====
24
  SEED_TOPICS = [
25
+ # ==== ๋Œ„์Šค / ๋ฐˆ / ์ฒผ๋ฆฐ์ง€ ====
26
+ "AI ๋ฏธ๋…€๊ฐ€ K-ํŒ ์•ˆ๋ฌด๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ์†Œํ™”ํ•œ๋‹ค๋ฉด",
27
+ "AI ๊ฑธ๊ทธ๋ฃน์ด ๋‰ด์ง„์Šค ๋Œ„์Šค๋ฅผ ์ปค๋ฒ„ํ•œ๋‹ค๋ฉด",
28
+ "AI ๋ชจ๋ธ์ด ํ‹ฑํ†ก ๋ฐˆ ๋Œ„์Šค์— ๋„์ „ํ•œ๋‹ค",
29
+ "AI ์บ๋ฆญํ„ฐ๊ฐ€ โ€˜ํ•˜์ž…๋ณด์ดโ€™์— ๋งž์ถฐ ์ถค์ถ˜๋‹ค๋ฉด",
30
+ "AI๊ฐ€ ์•„์ด๋Œ ์˜ค๋””์…˜์— ์ฐธ๊ฐ€ํ–ˆ๋‹ค๋ฉด",
31
+ "AI์™€ ์ธ๊ฐ„์ด ๋™์‹œ์— ์ถค์ถœ ๋•Œ ์ƒ๊ธฐ๋Š” ํ˜„์ƒ",
32
+ "AI ์•„์ด๋Œ์˜ ๋ฌด๋Œ€ ๋ฐฑ์—…๋Œ„์„œ๊ฐ€ ์ธ๊ฐ„์ด๋ผ๋ฉด",
33
+ "AI ์ธํ”Œ๋ฃจ์–ธ์„œ๊ฐ€ ๋ฆด์Šค ์ฒผ๋ฆฐ์ง€์— ์ฐธ์—ฌํ•œ๋‹ค๋ฉด",
34
+ "AI๊ฐ€ ํ‘œ์ • ํ•˜๋‚˜๋กœ ๋ฐˆ์„ ๋งŒ๋“ ๋‹ค๋ฉด",
35
+ "AI๊ฐ€ ๋žœ๋ค ๋…ธ๋ž˜ ์Šค์œ„์น˜ ๋Œ„์Šค์— ๋„์ „ํ•œ๋‹ค๋ฉด",
36
+ "AI๊ฐ€ ์œ ๋ช… ๋Œ„์Šค ์•ˆ๋ฌด๋ฅผ ์ดˆ์Šฌ๋กœ๋กœ ๋ณด์—ฌ์ค€๋‹ค๋ฉด",
37
+ "AI ์บ๋ฆญํ„ฐ๊ฐ€ ๊ตญ๊ฐ€๋ณ„ ์ „ํ†ต์ถค์„ ์ปค๋ฒ„ํ•œ๋‹ค๋ฉด",
38
+
39
+ # ==== ํŒจ์…˜ / ๋ทฐํ‹ฐ / ๋ผ์ดํ”„ ====
40
+ "AI ๋ชจ๋ธ์ด 2025 ํŒจ์…˜์œ„ํฌ๋ฅผ ์žฅ์•…ํ•œ ์ด์œ ",
41
+ "AI ๋ฏธ๋…€์˜ ํ•˜๋ฃจ: ์ถœ๊ทผ๋ถ€ํ„ฐ ๋ฐ์ดํŠธ๊นŒ์ง€",
42
+ "AI ์ธํ”Œ๋ฃจ์–ธ์„œ์˜ ์…€์นด ๋ฃจํ‹ด (ํ˜„์‹ค๊ฐ 200%)",
43
+ "AI๊ฐ€ ์ง์ ‘ ๊ณ ๋ฅธ ์˜ค๋Š˜์˜ ๋ฐ์ผ๋ฆฌ๋ฃฉ",
44
+ "AI ์บ๋ฆญํ„ฐ๊ฐ€ ๊พธ๋ฏผ ๋ฏธ๋‹ˆ๋ฉ€ ์ธํ…Œ๋ฆฌ์–ด",
45
+ "AI ์—ฌ์นœ์˜ VLOG: ๋‚˜๋ž‘ ํ•˜๋ฃจ ์‚ด์•„๋ณด๊ธฐ",
46
+ "AI๊ฐ€ ์ฝ”๋””ํ•œ ๋‚จ์ž ํŒจ์…˜, ํ˜„์‹ค๋ณด๋‹ค ๋‚ซ๋‹ค",
47
+ "AI ํ—ค์–ด์Šคํƒ€์ผ ์ถ”์ฒœ TOP5",
48
+ "AI๊ฐ€ ์•Œ๋ ค์ฃผ๋Š” ์—ฌ๋ฆ„ ๋ฐ์ดํŠธ๋ฃฉ ๊ฟ€์กฐํ•ฉ",
49
+ "AI๋กœ ๋งŒ๋“  ์ด์ƒํ˜• ์–ผ๊ตด, ์‹ค์ œ๋กœ ๋ณด๋ฉด ์ด๋Ÿฐ ๋А๋‚Œ",
50
+ "AI ๋ชจ๋ธ์ด ํŒจ์…˜์‡ผ๋ฅผ ์žฅ์•…ํ•œ ๋‚ ",
51
+ "AI๊ฐ€ ๊ณ„์ ˆ๋ณ„ ์ฝ”๋”” 10์ดˆ ์…”ํ”Œ์„ ๋ณด์—ฌ์ค€๋‹ค๋ฉด",
52
+
53
+ # ==== ์ผ์ƒ / ๋“œ๋ผ๋งˆํ‹ฑ / ๋ฆฌ์–ผ๋ฃจํ‹ด ====
54
+ "AI๋กœ ๋ฐ”๋€ ๋‚ด ํ•˜๋ฃจ, ์ถœ๊ทผ๋ถ€ํ„ฐ ์ž ๋“ค๊ธฐ๊นŒ์ง€",
55
+ "AI๊ฐ€ ์ธ๊ฐ„์˜ ํ•˜๋ฃจ๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ๋ณต์ œํ•œ๋‹ค๋ฉด",
56
+ "AI๊ฐ€ ๋‚˜ ๋Œ€์‹  ์ถœ๊ทผํ•˜๋ฉด ์ƒ๊ธฐ๋Š” ์ผ",
57
+ "AI ์—ฌ์ž์นœ๊ตฌ์™€ ํ˜„์‹ค ๋‚จ์ž์˜ ํ•˜๋ฃจ ๋ธŒ์ด๋กœ๊ทธ",
58
+ "AI๊ฐ€ ์ง‘์•ˆ์ผ์„ ๋งก์œผ๋ฉด ์ง„์งœ ํŽธํ• ๊นŒ",
59
+ "AI๊ฐ€ ๋ฐ˜๋ ค๊ฒฌ ์‚ฐ์ฑ…์„ ๋Œ€์‹ ํ•˜๋ฉด ๋ฒŒ์–ด์ง€๋Š” ์ผ",
60
+ "AI ์ธํ”Œ๋ฃจ์–ธ์„œ์˜ ๋ชจ๋‹ ๋ฃจํ‹ด์ด ์‹ค์ œ๋กœ ์กด์žฌํ•œ๋‹ค๋ฉด",
61
+ "AI๊ฐ€ ์ง์ ‘ ๋งŒ๋“  ์š”๋ฆฌ, ๋ง›์žˆ์„๊นŒ?",
62
+ "AI๊ฐ€ ์ฃผ์‹ ํˆฌ์žํ•˜๋ฉด ์–ผ๋งˆ๋‚˜ ๋ฒŒ๊นŒ?",
63
+ "AI ๋น„์„œ์™€ 24์‹œ๊ฐ„ ํ•จ๊ป˜ ์‚ด์•„๋ณด๊ธฐ",
64
+ "AI๊ฐ€ ๋‚˜ ๋Œ€์‹  ํšŒ์˜์— ์ฐธ์„ํ•œ๋‹ค๋ฉด",
65
+ "AI๊ฐ€ ์—ฌํ–‰ ์ฝ”์Šค๋ฅผ ํ•˜๋ฃจ ๋งŒ์— ์งœ์ค€๋‹ค๋ฉด",
66
+
67
+ # ==== ์—ฐ์•  / ๊ฐ์ • / ๋ฆฌ์•ก์…˜ ====
68
+ "AI ์—ฌ์ž์นœ๊ตฌ๊ฐ€ ์งˆํˆฌ๋ฅผ ๋А๋‚€๋‹ค๋ฉด",
69
+ "AI ๋‚จ์นœ์ด ์นดํ†กๆ—ข่ชญ็„ก่ฆ– ํ•œ๋‹ค๋ฉด",
70
+ "AI ์บ๋ฆญํ„ฐ๊ฐ€ ๊ณ ๋ฐฑ์„ ๋ฐ›์•„์ค„๊นŒ?",
71
+ "AI์™€ ์ธ๊ฐ„์˜ ์ฒซ ๋ฐ์ดํŠธ (์‹œ๋ฎฌ๋ ˆ์ด์…˜)",
72
+ "AI๊ฐ€ ์—ฐ์•  ์กฐ์–ธ์„ ํ•ด์ค€๋‹ค๋ฉด ์ง„์งœ ํ†ตํ• ๊นŒ?",
73
+ "AI๊ฐ€ ์šธ๋ฉด ์ง„์งœ ๊ฐ์ •์ผ๊นŒ?",
74
+ "AI๊ฐ€ ์‚ฌ๋ž‘์„ ๋ฐฐ์šด๋‹ค๋ฉด ์ƒ๊ธฐ๋Š” ๋ณ€ํ™”",
75
+ "AI๊ฐ€ ํ—ค์–ด์ง„ ์—ฐ์ธ์„ ๋‹ค์‹œ ๋งŒ๋‚ฌ์„ ๋•Œ",
76
+ "AI๊ฐ€ ๋‚ด ์‚ฌ์ง„์œผ๋กœ ๋‚˜๋ฅผ ์‚ฌ๋ž‘ํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด",
77
+ "AI์™€ ์ธ๊ฐ„์ด ์‹ธ์šฐ๋ฉด ๋ˆ„๊ฐ€ ๋จผ์ € ์‚ฌ๊ณผํ• ๊นŒ?",
78
+ "AI๊ฐ€ ์†Œ๊ฐœํŒ…์—์„œ ๋ง์‹ค์ˆ˜ํ•œ๋‹ค๋ฉด",
79
+ "AI๊ฐ€ ์ปคํ”Œ ์‚ฌ์ง„์„ ์žฌํ•ด์„ํ•ด ์ค€๋‹ค๋ฉด",
80
+
81
+ # ==== ์Œ์‹ / ์ฑŒ๋ฆฐ์ง€ / ๊ฟ€์กฐํ•ฉ ====
82
+ "ํŽธ์˜์  ๊ฟ€์กฐํ•ฉ TOP5, ์ง„์งœ ๋ฐ˜์น™๊ธ‰ ์กฐํ•ฉ ๋ชจ์Œ",
83
+ "AI ์…ฐํ”„๊ฐ€ ๋งŒ๋“  ๋ผ๋ฉด ๊ฟ€์กฐํ•ฉ",
84
+ "AI๊ฐ€ ์Œ์‹์‚ฌ์ง„์„ ๋ณด๊ณ  ๋ ˆ์‹œํ”ผ๋ฅผ ๋งŒ๋“ ๋‹ค๋ฉด",
85
+ "AI ๋จน๋ฐฉ: ์ธ๊ฐ„๋ณด๋‹ค ๋” ๋ฆฌ์–ผํ•˜๊ฒŒ ๋จน๋Š”๋‹ค",
86
+ "AI ์บ๋ฆญํ„ฐ๊ฐ€ ์นดํŽ˜ ์Œ๋ฃŒ ์ถ”์ฒœํ•ด์ค€๋‹ค",
87
+ "AI๊ฐ€ ๊ฐ€์žฅ ์ข‹์•„ํ•˜๋Š” ๊ฐ„์‹ TOP3",
88
+ "AI ๋‹ค์ด์–ดํŠธ ๋ฃจํ‹ด, ํ˜„์‹ค๋ณด๋‹ค ์ •ํ™•ํ•˜๋‹ค",
89
+ "AI๊ฐ€ ์š”๋ฆฌํ•˜๋ฉด ์ง„์งœ ๋ง›์žˆ์„๊นŒ?",
90
+ "AI๊ฐ€ ๊ณ ๋ฅธ ํŽธ์˜์  ๋„์‹œ๋ฝ 1๋“ฑ ๋ฉ”๋‰ด",
91
+ "AI๊ฐ€ ํ•œ๊ตญ ์Œ์‹ ์ฒ˜์Œ ๋จน์—ˆ์„ ๋•Œ ๋ฐ˜์‘",
92
+ "AI๊ฐ€ 10๋ถ„ ๋งŒ์— ํ™ˆ์นดํŽ˜๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค๋ฉด",
93
+ "AI๊ฐ€ ์„ธ๊ณ„ ๊ธธ๊ฑฐ๋ฆฌ ์Œ์‹์„ ์žฌํ˜„ํ•œ๋‹ค๋ฉด",
94
+
95
+ # ==== ํ•™์Šต / ์ž๊ธฐ๊ณ„๋ฐœ / ์ง€์‹ ====
96
+ "ํ•œ ๋‹ฌ ๋งŒ์— ์˜์–ด ๋ฆฌ์Šค๋‹ ํญ๋ฐœ์ ์œผ๋กœ ์˜ฌ๋ฆฌ๋Š” ๋ฒ•",
97
+ "AI๊ฐ€ ์•Œ๋ ค์ฃผ๋Š” ๊ณต๋ถ€ ์ง‘์ค‘๋ ฅ ๋ฃจํ‹ด",
98
+ "AI๊ฐ€ ์ง์ ‘ ์„ค๊ณ„ํ•œ โ€˜๊ธฐ์–ต๋ ฅ ํ›ˆ๋ จ๋ฒ•โ€™",
99
+ "AI๊ฐ€ ์ฝ”๋”ฉ ๋ฐฐ์›Œ์„œ ๋งŒ๋“  ์ฒซ ํ”„๋กœ์ ํŠธ",
100
+ "AI ํŠœํ„ฐ์™€ ํ•จ๊ป˜ ์˜์–ดํšŒํ™” ์—ฐ์Šตํ•˜๊ธฐ",
101
+ "AI๊ฐ€ ์Šค๋งˆํŠธํฐ ์ค‘๋…์„ ๊ณ ์น˜๋Š” ๋ฒ•",
102
+ "AI๊ฐ€ ์ธ๊ฐ„๋ณด๋‹ค ๋น ๋ฅด๊ฒŒ ์ฑ… ์š”์•ฝํ•˜๋Š” ๋ฒ•",
103
+ "AI๊ฐ€ ๋งŒ๋“  ์‹œํ—˜ ๋Œ€๋น„ ๊ณต๏ฟฝ๏ฟฝ ๋ฃจํ‹ด",
104
+ "AI๊ฐ€ ๋‡Œ ๊ณผํ•™์œผ๋กœ ์ง‘์ค‘๋ ฅ์„ ๋†’์ธ๋‹ค",
105
+ "AI ์„ ์ƒ๋‹˜์ด ์•Œ๋ ค์ฃผ๋Š” ํ† ์ต ๋ฆฌ์Šค๋‹ ๋น„๋ฒ•",
106
+ "AI๊ฐ€ ํ•˜๋ฃจ 10๋ถ„ ์Šคํ”ผํ‚น ํ›ˆ๋ จ ๋ฃจํ‹ด์„ ์งœ์ค€๋‹ค๋ฉด",
107
+
108
+ # ==== ์‚ฌํšŒ / ๋‰ด์Šค / ๋ฐ˜์ „ ====
109
+ "AI๊ฐ€ ๋งŒ๋“  ๊ฐ€์งœ ๋‰ด์Šค, ๋‹น์‹ ์€ ์†์„๊นŒ",
110
+ "AI ์บ๋ฆญํ„ฐ๊ฐ€ ๋Œ€ํ†ต๋ น์— ์ถœ๋งˆํ•œ๋‹ค๋ฉด",
111
+ "AI ๊ธฐ์ž๊ฐ€ ์ง์ ‘ ๋‰ด์Šค ์ง„ํ–‰์„ ๋งก๋Š”๋‹ค๋ฉด",
112
+ "AI๊ฐ€ ์ธํ”Œ๋ฃจ์–ธ์„œ๋ฅผ ๋Œ€์ฒดํ•œ ์„ธ์ƒ",
113
+ "AI๊ฐ€ ๋งŒ๋“  ๊ด‘๊ณ , ์ง„์งœ๋ณด๋‹ค ์„ค๋“๋ ฅ ์žˆ๋‹ค",
114
+ "AI ์บ๋ฆญํ„ฐ๊ฐ€ ํ† ๋ก  ํ”„๋กœ๊ทธ๋žจ์— ๋‚˜์˜จ๋‹ค๋ฉด",
115
+ "AI ๋ชจ๋ธ์ด SNS๋ฅผ ์ ๋ นํ•˜๋Š” ์ด์œ ",
116
+ "AI๊ฐ€ ์„ ๊ฑฐ์— ๊ฐœ์ž…ํ•œ๋‹ค๋ฉด ์ƒ๊ธธ ์ผ",
117
+ "AI์˜ ๊ฐ์ • ํ‘œํ˜„์ด ์ง„์งœ์ฒ˜๋Ÿผ ๋ณด์ผ ๋•Œ",
118
+ "ํŠน์ด์ ์ด ์˜จ AI ์„ธ์ƒ, ์›ƒ๊ธฐ์ง€๋งŒ ๋ฌด์„ญ๋‹ค",
119
+ "AI๋กœ ๋งŒ๋“  ์˜ํ™” ์˜ˆ๊ณ ํŽธ์ด ์ง„์งœ ์˜ํ™”๋ณด๋‹ค ๋ฉ‹์ง€๋‹ค",
120
+ "๊ฐค๋Ÿญ์‹œ vs ์•„์ดํฐ ์นด๋ฉ”๋ผ, ์ง„์งœ ์ฐจ์ด 1์ดˆ ๋น„๊ต",
121
+ "AI ์•„์ด๋Œ์ด ๋ถ€๋ฅด๋Š” K-POP ์ปค๋ฒ„ ๋ฌด๋Œ€",
122
+ "AI๋กœ ๋งŒ๋“  ๊ฐ€์ƒ ์•„์ด๋Œ, ์ง„์งœ๋ณด๋‹ค ์˜ˆ์œ ์ด์œ ",
123
  ]
124
  _seed_iter = itertools.cycle(SEED_TOPICS)
125
 
126
  # ===== ์š”์ฒญ/์‘๋‹ต =====
127
  class GenerateReq(BaseModel):
128
+ mode: Optional[str] = Field(None, description="auto | topic")
129
  topic: Optional[str] = None
130
  count: int = Field(30, ge=1, le=80, description="์ƒ์„ฑํ•  ์ œ๋ชฉ ์ˆ˜(์ตœ๋Œ€ 80)")
131
  # ์–ธ์–ด/๊ตญ๊ฐ€ (ํ˜„์ง€ํ™”)
 
151
  country: str
152
  category: Optional[str] = None
153
 
154
+ class ShortsPromptReq(BaseModel):
155
+ topic: str = Field(..., description="์ฃผ์ œ(์˜ˆ: ํŽธ์˜์  ๊ฟ€์กฐํ•ฉ TOP5)")
156
+ title: str = Field(..., description="์„ ์ •๋œ ์ œ๋ชฉ(ํ•œ ์ค„)")
157
+ lang: str = Field("ko")
158
+ style: str = Field("meme-shock")
159
+ tone: Optional[str] = Field(None, description="energetic | wholesome | informative | edgy ๋“ฑ")
160
+ duration_sec: int = Field(30, ge=15, le=60)
161
+ cuts: int = Field(5, ge=5, le=8)
162
+ emoji: int = Field(1, ge=0, le=2)
163
+
164
+ class ShortsPromptResp(BaseModel):
165
+ topic: str
166
+ title: str
167
+ prompt: str
168
+ lang: str
169
+ style: str
170
+ cuts: int
171
+ duration_sec: int
172
+
173
  # ===== ์–ธ์–ด/๊ตญ๊ฐ€ ์ •๊ทœํ™” =====
174
  LANG_MAP = {
175
  "ko": ("ko", "ํ•œ๊ตญ์–ด"), "ko-kr": ("ko", "ํ•œ๊ตญ์–ด"), "korean": ("ko", "ํ•œ๊ตญ์–ด"), "ํ•œ๊ตญ์–ด": ("ko", "ํ•œ๊ตญ์–ด"),
 
178
  "es": ("es", "Espaรฑol"), "es-es": ("es", "Espaรฑol"), "es-mx": ("es", "Espaรฑol"), "spanish": ("es", "Espaรฑol"), "์ŠคํŽ˜์ธ์–ด": ("es", "Espaรฑol"),
179
  "zh": ("zh", "ไธญๆ–‡"), "zh-cn": ("zh", "ไธญๆ–‡"), "chinese": ("zh", "ไธญๆ–‡"), "์ค‘๊ตญ์–ด": ("zh", "ไธญๆ–‡"), "ไธญๆ–‡": ("zh", "ไธญๆ–‡"),
180
  }
 
 
181
  PNT_MAP = {
182
  "KR": ("south_korea", "ko-KR"),
183
  "US": ("united_states", "en-US"),
 
186
  "MX": ("mexico", "es-419"),
187
  "CN": ("china", "zh-CN"), # pytrends ๋ฏธ์ง€์›/๋ถˆ์•ˆ์ • ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ
188
  }
 
189
  LANG_DEFAULT_COUNTRY = {"ko": "KR", "en": "US", "ja": "JP", "es": "ES", "zh": "CN"}
190
 
191
  def normalize_lang_and_country(lang_in: str, country_in: Optional[str]) -> Tuple[str, str, str]:
 
219
  "AIไธ€ๅˆ†้’Ÿๅš็ˆ†ๆฌพ็Ÿญ่ง†้ข‘๏ผŒ็œŸ็š„ๅ‡็š„๏ผŸ\n"
220
  "ไพฟๅˆฉๅบ—็ฅž็ป„ๅˆTOP5๏ผŒๆœ€ๅŽไธ€ไธช็›ดๆŽฅ็‚ธ่ฃ‚๏ผ\n"),
221
  }
 
222
  HOOKS = {
223
  "ko": ["์ด๊ฑด ๋ชฐ๋ž์ฃ ?", "์†Œ๋ฆ„", "์ถฉ๊ฒฉ", "์‹คํ™”๋ƒ", "TOP5", "ใ„นใ…‡", "๋Œ€๋ฐ•", "๋ฏธ์ณค๋‹ค", "๋ฐ˜์ „ ์žˆ์Œ", "ํ•œ์ค„ ์š”์•ฝ", "์ง€๊ธˆ ์•ˆ ๋ณด๋ฉด ์†ํ•ด", "30์ดˆ ์ปท", "๋๊นŒ์ง€ ๋ณด์„ธ์š”", "์•Œ๊ณ  ๋ณด๋ฉด ๋” ์†Œ๋ฆ„", "์ดˆ๊ฐ„๋‹จ"],
224
  "en": ["did you know?", "shocking", "insane", "is this real?", "TOP 5", "no way", "viral", "mind-blown", "plot twist", "one-line tip", "donโ€™t miss this", "30s hack", "watch till the end", "low-key genius"],
 
226
  "es": ["ยฟlo sabรญas?", "impactante", "una locura", "ยฟes real?", "TOP 5", "no puede ser", "viral", "mente en blanco", "giro inesperado", "tip en una lรญnea", "no te lo pierdas", "truco de 30s", "hasta el final", "dato clave"],
227
  "zh": ["้œ‡ๆƒŠ", "ๅคช็ฆป่ฐฑไบ†ๅง", "ไฝ ็ปๅฏนๆƒณไธๅˆฐ", "TOP5", "็ฝ‘ๅ‹้ƒฝๆƒŠๅ‘†ไบ†", "ๅคช็‚ธไบ†", "็ฌ‘ๆญป", "็ ด้˜ฒไบ†", "่ฟ™ๆ“ไฝœ้€†ๅคฉ", "็œŸๆ•ขๆ‹", "ไธ็œ‹ๅŽๆ‚”", "็ป†ๆ€ๆžๆ", "็ปไบ†", "ๅคช็‹ ไบ†"],
228
  }
 
229
  TAILS = {
230
  "ko": ["๋ฐฉ๋ฒ• ๊ณต๊ฐœ", "์‹œ์ฒญ์ž ํ›„๊ธฐ ํ„ฐ์ง", "์ „/ํ›„ ๋น„๊ต", "์ดˆ๋ณด๋„ ๊ฐ€๋Šฅ", "์ˆจ์€ ๊ธฐ๋Šฅ", "๋ฐ˜์ „ ์ฃผ์˜", "์‹ค์ „ ๊ฟ€ํŒ", "๋ชจ๋ฅด๋ฉด ์†ํ•ด", "ํ•œ ๋ฒˆ์— ๋", "๋ฐ”๋กœ ์จ๋จน๋Š” ์Šคํ‚ฌ", "ํ•ต์‹ฌ๋งŒ ์š”์•ฝ", "์ •๋ฆฌํ•ด๋“œ๋ฆผ"],
231
  "en": ["method revealed", "viewers went crazy", "before vs after", "beginner friendly", "hidden feature", "twist ahead", "pro tips", "donโ€™t miss out", "done in one go", "instant skill", "key points only", "quick recap"],
 
233
  "es": ["mรฉtodo revelado", "los viewers enloquecen", "antes vs despuรฉs", "para principiantes", "funciรณn oculta", "plot twist", "tips pro", "no te lo pierdas", "todo de una", "skill al instante", "puntos clave", "resumen rรกpido"],
234
  "zh": ["ๅฎŒๆ•ด็‰ˆๆฅไบ†", "็ฝ‘ๅ‹็ƒญ่ฎฎไธญ", "็œ‹ๅฎŒๆˆ‘ๆฒ‰้ป˜ไบ†", "ๅ‰ๅŽๅฏนๆฏ”็ฆป่ฐฑ", "ๆ–ฐๆ‰‹ไนŸ่ƒฝๅš", "้š่—ๅŠŸ่ƒฝๆ›ๅ…‰", "่ƒŒๅŽๅŽŸๅ› ้œ‡ๆ’ผ", "ไธ็œ‹็œŸ็š„ไบ", "ๆ•™็จ‹ๆฅไบ†", "ๅ…จ็ฝ‘็ˆ†็ซ", "ไบฒๆต‹ๆœ‰ๆ•ˆ", "ๅนฒ่ดงๆ€ป็ป“"],
235
  }
 
236
  PROMPT_HEADER = {
237
  "ko": "์—ญํ• : ๋‹น์‹ ์€ ํ•ด๋‹น ๊ตญ๊ฐ€์˜ ์œ ํŠœ๋ธŒ ์‡ผ์ธ  ์นดํ”ผ๋ผ์ดํ„ฐ๋‹ค.\n๊ณผ์—…: ์•„๋ž˜ โ€˜์ฃผ์ œโ€™๋กœ ํ˜„์ง€ ์‹œ์ฒญ์ž๊ฐ€ ๋งŽ์ด ๋ณด๊ฒŒ๋” ๋ฐ”์ด๋Ÿด ์ œ๋ชฉ์„ ์ƒ์„ฑํ•˜๋ผ.\n",
238
  "en": "Role: You are a YouTube Shorts copywriter for the target country.\nTask: Generate viral titles so local viewers want to watch.\n",
 
335
  r'^\s*[-โ€ข]\s*$',
336
  r'^\s*๊ฐ ์ œ๋ชฉ์€',
337
  ]
 
338
  def _fingerprint(s: str) -> str:
339
  return re.sub(r"[\s\W]+", "", s.lower())
 
340
  def _valid_len(s: str, lang_code: str) -> bool:
341
  if lang_code in ("en", "es"):
342
  words = re.findall(r"\w+", s)
343
  return 3 <= len(words) <= 14
344
  return 6 <= len(s) <= 36 # ko/ja/zh
 
345
  def _leak(s: str) -> bool:
346
  for p in _LEAK_PATTERNS:
347
  if re.search(p, s, flags=re.I):
348
  return True
349
  return False
 
350
  def postprocess(raw: str, lang_code: str, numberless: bool = True) -> List[str]:
351
  if not raw: return []
352
  raw = re.sub(r"<extra_id_\d+>", "", raw)
 
412
 
413
  # ===== ํ† ํ”ฝ ํฌ์ง€(์˜์ƒํ™” ๊ฐ€๋Šฅํ•œ ์ฃผ์ œ ์ž๋™ ์ƒ์„ฑ) =====
414
  TOPIC_CATEGORIES = ("ai-slice","ai-teach","ai-transform","ai-battle","ai-micro","mixed")
 
415
  OBJ = {
416
  "ko": {
417
  "fruit": ["์ˆ˜๋ฐ•","๋ง๊ณ ","ํŒŒ์ธ์• ํ”Œ","์‚ฌ๊ณผ","ํ‚ค์œ„","์šฉ๊ณผ","๋ณต์ˆญ์•„","๋ฐ”๋‚˜๋‚˜","์ฝ”์ฝ”๋„›","ํฌ๋„"],
 
436
  }
437
  def _obj(lang: str) -> dict:
438
  return OBJ.get(lang, OBJ["ko"])
 
439
  def forge_topic(lang_code: str, category: str) -> str:
440
  o = _obj(lang_code)
441
  if category == "ai-slice":
 
459
  return f"AI ๋งˆ์ดํฌ๋กœ ์„ธ๊ณ„ ํƒํ—˜: {t} ์† {m} ํ™•๋Œ€ ๊ด€์ฐฐ"
460
  return forge_topic(lang_code, random.choice(TOPIC_CATEGORIES[:-1])) # mixed
461
 
462
+ # ===== ์‡ผ์ธ  ํ”„๋กฌํ”„ํŠธ(์ƒท๋ฆฌ์ŠคํŠธ) ์ƒ์„ฑ =====
463
+ def build_shorts_prompt(topic: str, title: str, lang_code: str, style: str, tone: Optional[str], cuts: int, duration_sec: int, emoji: int) -> str:
464
+ # ์ปท ๊ฐœ์ˆ˜ ๋ณด์ •
465
+ cuts = max(5, min(8, cuts))
466
+ per_cut = max(4, min(7, duration_sec // cuts)) # ์ปท๋‹น ์ดˆ
467
+ style_note = {
468
+ "meme-shock": "๊ฐ•ํ•œ ํ›… + ๋ฐ˜์ „ + ๋ฆฌ๋“ฌ. ์ž๋ง‰ ์นœํ™”. ์งง์€ ๊ตฌ/๋ช…๋ น๋ฌธ.",
469
+ "wholesome": "๊ณต๊ฐ/ํž๋ง ํ†ค. ์นœ์ ˆํ•œ ๋งํˆฌ. ํฌ๋ง/์‘์›.",
470
+ "informative": "ํ•ต์‹ฌ ๋จผ์ €, ํŒฉํŠธ ์ค‘์‹ฌ. ๊ฐ„๊ฒฐ/์‹ ๋ขฐ.",
471
+ "clickbait": "๊ถ๊ธˆ์ฆ ์œ ๋ฐœ, ๊ณผ์žฅ ํ—ˆ์šฉ(์‚ฌ์‹ค ์™œ๊ณก ๊ธˆ์ง€).",
472
+ }.get(style, "๋ฆฌ๋“ฌยทํ˜ธ๊ธฐ์‹ฌ ์ค‘์‹ฌ.")
473
+ tone_note = f"์ถ”๊ฐ€ ํ†ค: {tone}." if tone else ""
474
+ emj = " ์ด๋ชจ์ง€ ์ ๋‹นํžˆ ์‚ฌ์šฉ" if emoji > 0 else " ์ด๋ชจ์ง€ ์‚ฌ์šฉ ๊ธˆ์ง€"
475
+
476
+ header = (
477
+ f"์—ญํ• : ์œ ํŠœ๋ธŒ ์‡ผ์ธ  ์ฝ˜ํ…์ธ  ํ”Œ๋ž˜๋„ˆ & ์นดํ”ผ๋ผ์ดํ„ฐ\n"
478
+ f"๋ชฉํ‘œ: ์•„๋ž˜ '์ œ๋ชฉ'์„ ๊ธฐ๋ฐ˜์œผ๋กœ {duration_sec}์ดˆ ์ด๋‚ด ์˜์ƒ ํ”„๋กฌํ”„ํŠธ(๋Œ€๋ณธ ์ง€์‹œ๋ฌธ)๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.\n"
479
+ f"๊ทœ์น™:\n"
480
+ f"- ์ดˆ๋ฐ˜ 3์ดˆ ๊ฐ•ํ•œ ํ›…, 3~4๊ฐœ์˜ ํ•ต์‹ฌ ํฌ์ธํŠธ, ๋งˆ์ง€๋ง‰์— ์•ก์…˜์ฝœ(์ข‹์•„์š”/๋Œ“๊ธ€ ์œ ๋„)\n"
481
+ f"- ์งง๊ณ  ๋ฆฌ๋“ฌ๊ฐ ์žˆ๊ฒŒ, ์ž๋ง‰์šฉ ๋ฌธ์žฅ\n"
482
+ f"- ์ปท(์žฅ๋ฉด)์„ ๋ฒˆํ˜ธ๋กœ ๊ตฌ๋ถ„, ๊ฐ ์ปท 1~2๋ฌธ์žฅ\n"
483
+ f"- ์ดฌ์˜ ์†Œ์Šค ์—†์–ด๋„ ์ƒ์„ฑํ˜• ์˜์ƒํˆด์— ๋ฐ”๋กœ ๋„ฃ์„ ์ˆ˜ ์žˆ๋Š” ๋ฌ˜์‚ฌ ํฌํ•จ\n"
484
+ f"- ์Šคํƒ€์ผ: {style} ({style_note}), ์–ธ์–ด: {lang_code}, {tone_note}{emj}\n"
485
+ f"- ์ปท ์ˆ˜: {cuts}๊ฐœ, ์ปท๋‹น {per_cut}์ดˆ ๋‚ด์™ธ\n\n"
486
+ f"๋ฉ”ํƒ€:\n"
487
+ f"โ€ข ์ฃผ์ œ(Topic): {topic}\n"
488
+ f"โ€ข ์ œ๋ชฉ(Title): {title}\n\n"
489
+ f"์ถœ๋ ฅ ํฌ๋งท ์˜ˆ์‹œ(ํ˜•์‹๋งŒ ์ฐธ๊ณ ):\n"
490
+ f"1) ํ›…: ์‹œ์ฒญ์ž ํ˜ธ๊ธฐ์‹ฌ ํญ๋ฐœ ํ•œ ์ค„\n"
491
+ f"2) ํฌ์ธํŠธ1: ๊ตฌ์ฒด ์˜ˆ์‹œ/๋ธŒ๋žœ๋“œ/๊ฐ€๊ฒฉ ๋“ฑ\n"
492
+ f"3) ํฌ์ธํŠธ2: ๋น„๊ต/๋ฐ˜์ „/์ฃผ์˜\n"
493
+ f"4) ํฌ์ธํŠธ3: ์‹ค์ „ ํŒ/๋ฐ”๋กœ ์จ๋จน๊ธฐ\n"
494
+ f"5) ๋งˆ๋ฌด๋ฆฌ: ํ•œ ์ค„ ์š”์•ฝ + ์•ก์…˜์ฝœ(๋Œ“๊ธ€/์ข‹์•„์š” ์œ ๋„)\n\n"
495
+ f"์ด์ œ ์œ„ ๊ทœ์น™์„ ์ฒ ์ €ํžˆ ์ง€์ผœ {cuts}๊ฐœ์˜ ์ปท์œผ๋กœ ํ•œ๊ตญ์–ด ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ถœ๋ ฅํ•˜๋ผ."
496
+ )
497
+ return header
498
+
499
+ # ===== ์—”๋“œํฌ์ธํŠธ =====
500
  @app.get("/topic/auto", response_model=TopicResp)
501
  def topic_auto(
502
  count: int = Query(10, ge=1, le=100),
 
524
 
525
  @app.get("/")
526
  def root():
527
+ return JSONResponse({"message": "NocoAI Title & Prompt Forge. Try GET /topic/auto, POST /generate, GET /generate/auto, POST /prompt/shorts"}, media_type="application/json; charset=utf-8")
528
 
529
  @app.get("/health")
530
  def health():
 
568
  titles, used_prompt = generate_titles(topic, count, req, lang_code, pretty_lang)
569
  used_source = trend_source if (trend_source == "pytrends" and HAS_PYTRENDS) else "seed"
570
  return JSONResponse({"topic": topic, "titles": titles, "used_prompt": used_prompt, "trend_source": used_source}, media_type="application/json; charset=utf-8")
571
+
572
+ @app.post("/prompt/shorts", response_model=ShortsPromptResp)
573
+ def prompt_shorts(body: ShortsPromptReq):
574
+ lang_code, _, _ = normalize_lang_and_country(body.lang, None)
575
+ prompt = build_shorts_prompt(
576
+ topic=body.topic,
577
+ title=body.title,
578
+ lang_code=lang_code,
579
+ style=body.style,
580
+ tone=body.tone,
581
+ cuts=body.cuts,
582
+ duration_sec=body.duration_sec,
583
+ emoji=body.emoji
584
+ )
585
+ return JSONResponse({
586
+ "topic": body.topic,
587
+ "title": body.title,
588
+ "prompt": prompt,
589
+ "lang": lang_code,
590
+ "style": body.style,
591
+ "cuts": body.cuts,
592
+ "duration_sec": body.duration_sec
593
+ }, media_type="application/json; charset=utf-8")