openfree commited on
Commit
6243bc6
ยท
verified ยท
1 Parent(s): 1236aa5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +290 -143
app.py CHANGED
@@ -81,7 +81,7 @@ BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
81
 
82
  @dataclass
83
  class ConversationConfig:
84
- max_words: int = 4000
85
  prefix_url: str = "https://r.jina.ai/"
86
  api_model_name: str = "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo"
87
  legacy_local_model_name: str = "NousResearch/Hermes-2-Pro-Llama-3-8B"
@@ -89,8 +89,10 @@ class ConversationConfig:
89
  local_model_name: str = "Private-BitSix-Mistral-Small-3.1-24B-Instruct-2503.gguf"
90
  local_model_repo: str = "ginigen/Private-BitSix-Mistral-Small-3.1-24B-Instruct-2503"
91
  # ํ† ํฐ ์ˆ˜ ์ฆ๊ฐ€
92
- max_tokens: int = 3000 # 2048์—์„œ 6000์œผ๋กœ ์ฆ๊ฐ€
93
- max_new_tokens: int = 6000 # 4000์—์„œ 8000์œผ๋กœ ์ฆ๊ฐ€
 
 
94
 
95
 
96
  def brave_search(query: str, count: int = 8, freshness_days: int | None = None):
@@ -119,20 +121,28 @@ def brave_search(query: str, count: int = 8, freshness_days: int | None = None):
119
  logging.error(f"Brave search error: {e}")
120
  return []
121
 
122
- def format_search_results(query: str) -> str:
123
- """๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ํฌ๋งทํŒ…ํ•˜์—ฌ ๋ฐ˜ํ™˜ (๊ฐ„๋žตํ•˜๊ฒŒ)"""
124
- rows = brave_search(query, 3, freshness_days=3) # 6๊ฐœ์—์„œ 3๊ฐœ๋กœ ์ค„์ž„
 
 
125
  if not rows:
126
- return "" # ๋นˆ ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜
127
 
128
- # ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋” ๊ฐ„๋žตํ•˜๊ฒŒ ์š”์•ฝ
129
  results = []
130
- for r in rows[:2]: # ์ตœ๋Œ€ 2๊ฐœ๋งŒ ์‚ฌ์šฉ
131
- # ์Šค๋‹ˆํŽซ์„ 100์ž๋กœ ์ œํ•œ
132
- snippet = r['snippet'][:100] + "..." if len(r['snippet']) > 100 else r['snippet']
133
- results.append(f"- {r['title']}: {snippet}")
 
 
 
 
 
 
 
134
 
135
- return "\n".join(results) + "\n"
136
 
137
  def extract_keywords_for_search(text: str, language: str = "English") -> List[str]:
138
  """ํ…์ŠคํŠธ์—์„œ ๊ฒ€์ƒ‰ํ•  ํ‚ค์›Œ๋“œ ์ถ”์ถœ (๊ฐœ์„ )"""
@@ -157,6 +167,47 @@ def extract_keywords_for_search(text: str, language: str = "English") -> List[st
157
  return [max(keywords, key=len)] # ๊ฐ€์žฅ ๊ธด ๋‹จ์–ด 1๊ฐœ
158
  return []
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  class UnifiedAudioConverter:
161
  def __init__(self, config: ConversationConfig):
162
  self.config = config
@@ -310,16 +361,25 @@ class UnifiedAudioConverter:
310
 
311
 
312
  def _build_prompt(self, text: str, language: str = "English", search_context: str = "") -> str:
313
- """Build prompt for conversation generation with search context"""
314
- # ํ…์ŠคํŠธ ๊ธธ์ด ์ œํ•œ์„ ๋” ๊ฐ•ํ•˜๊ฒŒ ์ ์šฉ
315
- max_text_length = 3000 if search_context else 4000
316
  if len(text) > max_text_length:
317
  text = text[:max_text_length] + "..."
318
 
319
  if language == "Korean":
 
320
  template = """
321
  {
322
  "conversation": [
 
 
 
 
 
 
 
 
323
  {"speaker": "์ค€์ˆ˜", "text": ""},
324
  {"speaker": "๋ฏผํ˜ธ", "text": ""},
325
  {"speaker": "์ค€์ˆ˜", "text": ""},
@@ -328,7 +388,6 @@ class UnifiedAudioConverter:
328
  }
329
  """
330
 
331
- # ๊ฒ€์ƒ‰ ์ปจํ…์ŠคํŠธ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ํฌํ•จ
332
  context_part = ""
333
  if search_context:
334
  context_part = f"# ์ตœ์‹  ๊ด€๋ จ ์ •๋ณด:\n{search_context}\n"
@@ -336,22 +395,38 @@ class UnifiedAudioConverter:
336
  base_prompt = (
337
  f"# ์›๋ณธ ์ฝ˜ํ…์ธ :\n{text}\n\n"
338
  f"{context_part}"
339
- f"์œ„ ๋‚ด์šฉ์œผ๋กœ ํ•œ๊ตญ์–ด ํŒŸ์บ์ŠคํŠธ ๋Œ€ํ™”๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”.\n\n"
340
  f"## ํ•ต์‹ฌ ์ง€์นจ:\n"
341
- f"- ์ค€์ˆ˜(์ง„ํ–‰์ž)์™€ ๋ฏผํ˜ธ(์ „๋ฌธ๊ฐ€) ๋‘ 30๋Œ€ ๋‚จ์„ฑ์˜ ๋Œ€ํ™”\n"
342
- f"- ์„œ๋กœ ์กด๋Œ“๋ง ์‚ฌ์šฉ ํ•„์ˆ˜ (๋ฐ˜๋ง ์ ˆ๋Œ€ ๊ธˆ์ง€)\n"
343
- f"- ์ž์—ฐ์Šค๋Ÿฌ์šด ํ•œ๊ตญ์–ด ํ‘œํ˜„ ์‚ฌ์šฉ\n"
344
- f"- ๊ฐ ๋Œ€ํ™” 2-3๋ฌธ์žฅ, ์ „์ฒด 8-10ํšŒ ์ฃผ๊ณ ๋ฐ›๊ธฐ\n"
345
- f"- ์ตœ์‹  ์ •๋ณด๊ฐ€ ์žˆ๋‹ค๋ฉด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํฌํ•จ\n\n"
 
 
 
 
 
 
 
346
  f"JSON ํ˜•์‹์œผ๋กœ๋งŒ ๋ฐ˜ํ™˜:\n{template}"
347
  )
348
 
349
  return base_prompt
350
 
351
  else:
 
352
  template = """
353
  {
354
  "conversation": [
 
 
 
 
 
 
 
 
355
  {"speaker": "Alex", "text": ""},
356
  {"speaker": "Jordan", "text": ""},
357
  {"speaker": "Alex", "text": ""},
@@ -367,13 +442,20 @@ class UnifiedAudioConverter:
367
  base_prompt = (
368
  f"# Content:\n{text}\n\n"
369
  f"{context_part}"
370
- f"Create a podcast conversation.\n\n"
371
- f"Guidelines:\n"
372
- f"- Alex (Host) and Jordan (Expert)\n"
373
- f"- Natural conversational English\n"
374
- f"- Each response 2-3 sentences\n"
375
- f"- 8-10 exchanges total\n"
376
- f"- Include latest info if available\n\n"
 
 
 
 
 
 
 
377
  f"Return JSON only:\n{template}"
378
  )
379
 
@@ -382,27 +464,30 @@ class UnifiedAudioConverter:
382
 
383
 
384
  def _build_messages_for_local(self, text: str, language: str = "English", search_context: str = "") -> List[Dict]:
385
- """Build messages for local LLM with enhanced Korean guidelines"""
386
  if language == "Korean":
387
  system_message = (
388
- "๋‹น์‹ ์€ ํ•œ๊ตญ ์ตœ๊ณ ์˜ ํŒŸ์บ์ŠคํŠธ ๋Œ€๋ณธ ์ „๋ฌธ ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค. "
389
- "ํ•œ๊ตญ์ธ์˜ ์ •์„œ์™€ ๋ฌธํ™”๋ฅผ ์™„๋ฒฝํžˆ ์ดํ•ดํ•˜๊ณ , 30๋Œ€ ํ•œ๊ตญ์ธ ์ฒญ์ทจ์ž๋“ค์ด ๋๊นŒ์ง€ ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๋Š” "
390
- "๋งค๋ ฅ์ ์ด๊ณ  ์œ ์ตํ•œ ๋Œ€ํ™”๋ฅผ ๋งŒ๋“ค์–ด๋ƒ…๋‹ˆ๋‹ค.\n\n"
391
  "ํ•ต์‹ฌ ์›์น™:\n"
392
- "1. ๋‘ ํ™”์ž๋Š” ๋ฐ˜๋“œ์‹œ ์„œ๋กœ์—๊ฒŒ ์กด๋Œ“๋ง์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค (๋ฐ˜๋ง ์ ˆ๋Œ€ ๊ธˆ์ง€)\n"
393
- "2. ํ•œ๊ตญ ๋ฌธํ™”์˜ ์ •์„œ์  ์ฝ”๋“œ์™€ ๊ฐ€์น˜๊ด€์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค\n"
394
- "3. ์‹ค์ œ ํ•œ๊ตญ์ธ๋“ค์ด ์ผ์ƒ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ž์—ฐ์Šค๋Ÿฌ์šด ํ‘œํ˜„์„ ๊ตฌ์‚ฌํ•ฉ๋‹ˆ๋‹ค\n"
395
- "4. ์ฒญ์ทจ์ž๊ฐ€ ๊ณต๊ฐํ•˜๊ณ  ์‹ค์šฉ์ ์œผ๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋‚ด์šฉ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค\n"
396
- "5. ์ตœ์‹  ์ •๋ณด์™€ ํŠธ๋ Œ๋“œ๋ฅผ ์ ์ ˆํžˆ ๋ฐ˜์˜ํ•˜์—ฌ ์‹œ์˜์„ฑ์„ ํ™•๋ณดํ•ฉ๋‹ˆ๋‹ค\n\n"
397
- "๋‹น์‹ ์˜ ๋Œ€๋ณธ์€ ํ•œ๊ตญ ํŒŸ์บ์ŠคํŠธ ์‹œ์žฅ์—์„œ ์ตœ๊ณ  ์ˆ˜์ค€์˜ ํ’ˆ์งˆ๋กœ ์ธ์ •๋ฐ›๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค."
398
  )
399
  else:
400
  system_message = (
401
- "You are an expert podcast scriptwriter who creates engaging, "
402
- "natural conversations that keep listeners hooked. "
403
- "You understand how to balance information with entertainment, "
404
- "using real conversational patterns and authentic reactions. "
405
- "You excel at incorporating current information and trends to make content relevant and timely."
 
 
 
 
406
  )
407
 
408
  return [
@@ -412,15 +497,14 @@ class UnifiedAudioConverter:
412
 
413
  @spaces.GPU(duration=120)
414
  def extract_conversation_local(self, text: str, language: str = "English", progress=None) -> Dict:
415
- """Extract conversation using new local LLM with search context"""
416
  try:
417
- # ๊ฒ€์ƒ‰ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ
418
  search_context = ""
419
- if BRAVE_KEY:
420
  try:
421
  keywords = extract_keywords_for_search(text, language)
422
  if keywords:
423
- # ์ฒซ ๋ฒˆ์งธ ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰
424
  search_query = keywords[0] if language == "Korean" else f"{keywords[0]} latest news"
425
  search_context = format_search_results(search_query)
426
  print(f"Search context added for: {search_query}")
@@ -433,26 +517,31 @@ class UnifiedAudioConverter:
433
  chat_template = self._get_messages_formatter_type(self.config.local_model_name)
434
  provider = LlamaCppPythonProvider(self.local_llm)
435
 
436
- # ๊ฐ•ํ™”๋œ ์–ธ์–ด๋ณ„ ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€
437
  if language == "Korean":
438
  system_message = (
439
- "๋‹น์‹ ์€ ํ•œ๊ตญ์–ด ํŒŸ์บ์ŠคํŠธ ์ „๋ฌธ ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค. "
440
- "ํ•œ๊ตญ ์ฒญ์ทจ์ž๋“ค์˜ ๋ฌธํ™”์  ๋งฅ๋ฝ๊ณผ ์–ธ์–ด์  ํŠน์„ฑ์„ ์™„๋ฒฝํžˆ ์ดํ•ดํ•˜๊ณ , "
441
- "์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋งค๋ ฅ์ ์ธ ๋Œ€๋ณธ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. "
442
- "ํŠนํžˆ ๋‘ ํ™”์ž๊ฐ€ ์„œ๋กœ์—๊ฒŒ ์กด๋Œ“๋ง์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ํ•„์ˆ˜์ด๋ฉฐ, "
443
- "๋ฐ˜๋ง์€ ์ ˆ๋Œ€ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. "
444
- "์‹ค์ œ ํ•œ๊ตญ์ธ์ด ๋Œ€ํ™”ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ‘œํ˜„, ์ ์ ˆํ•œ ๊ฐํƒ„์‚ฌ, "
445
- "๋ฌธํ™”์ ์œผ๋กœ ์ ํ•ฉํ•œ ์˜ˆ์‹œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฒญ์ทจ์ž๊ฐ€ ๊ณต๊ฐํ•˜๊ณ  ๋ชฐ์ž…ํ•  ์ˆ˜ ์žˆ๋Š” "
446
- "๋Œ€ํ™”๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”. ์ตœ์‹  ์ •๋ณด๋„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ฐ˜์˜ํ•˜์„ธ์š”. JSON ํ˜•์‹์œผ๋กœ๋งŒ ์‘๋‹ตํ•˜์„ธ์š”."
 
 
447
  )
448
  else:
449
  system_message = (
450
- "You are an expert podcast scriptwriter specializing in creating "
451
- "engaging, natural conversations that captivate listeners. "
452
- "You excel at transforming complex information into accessible, "
453
- "entertaining dialogue while maintaining authenticity and educational value. "
454
- "Incorporate current trends and latest information naturally. "
455
- "Respond only in JSON format."
 
 
 
456
  )
457
 
458
  agent = LlamaCppAgent(
@@ -463,7 +552,7 @@ class UnifiedAudioConverter:
463
  )
464
 
465
  settings = provider.get_provider_default_settings()
466
- settings.temperature = 0.8 # ์•ฝ๊ฐ„ ๋†’์—ฌ์„œ ๋” ์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€ํ™” ์ƒ์„ฑ
467
  settings.top_k = 40
468
  settings.top_p = 0.95
469
  settings.max_tokens = self.config.max_tokens # ์ฆ๊ฐ€๋œ ํ† ํฐ ์ˆ˜ ์‚ฌ์šฉ
@@ -486,7 +575,12 @@ class UnifiedAudioConverter:
486
  json_match = re.search(pattern, response)
487
 
488
  if json_match:
489
- return json.loads(json_match.group())
 
 
 
 
 
490
  else:
491
  raise ValueError("No valid JSON found in local LLM response")
492
 
@@ -496,24 +590,24 @@ class UnifiedAudioConverter:
496
 
497
  @spaces.GPU(duration=120)
498
  def extract_conversation_legacy_local(self, text: str, language: str = "English", progress=None, search_context: str = "") -> Dict:
499
- """Extract conversation using legacy local model (fallback)"""
500
  try:
501
  self.initialize_legacy_local_mode()
502
 
503
- # ๊ฐ•ํ™”๋œ ์–ธ์–ด๋ณ„ ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€
504
  if language == "Korean":
505
  system_message = (
506
- "๋‹น์‹ ์€ ํ•œ๊ตญ์–ด ํŒŸ์บ์ŠคํŠธ ์ „๋ฌธ ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค. "
507
- "30๋Œ€ ํ•œ๊ตญ์ธ ์ฒญ์ทจ์ž๋ฅผ ๋Œ€์ƒ์œผ๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ํฅ๋ฏธ๋กœ์šด ๋Œ€ํ™”๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”. "
508
- "๋‘ ํ™”์ž๋Š” ๋ฐ˜๋“œ์‹œ ์„œ๋กœ์—๊ฒŒ ์กด๋Œ“๋ง์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ๋ฐ˜๋ง์€ ์ ˆ๋Œ€ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. "
509
- "์‹ค์ œ ์‚ฌ์šฉํ•˜๋Š” ํ•œ๊ตญ์–ด ํ‘œํ˜„๊ณผ ๋ฌธํ™”์  ๋งฅ๋ฝ์„ ๋ฐ˜์˜ํ•˜์—ฌ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. "
510
- "์ตœ์‹  ์ •๋ณด๋„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํฌํ•จ์‹œ์ผœ์ฃผ์„ธ์š”."
511
  )
512
  else:
513
  system_message = (
514
- "You are an expert podcast scriptwriter. "
515
- "Create natural, engaging conversations that inform and entertain listeners. "
516
- "Incorporate current information and trends naturally."
 
517
  )
518
 
519
  chat = [
@@ -540,7 +634,7 @@ class UnifiedAudioConverter:
540
  streamer=streamer,
541
  max_new_tokens=self.config.max_new_tokens, # ์ฆ๊ฐ€๋œ ํ† ํฐ ์ˆ˜ ์‚ฌ์šฉ
542
  do_sample=True,
543
- temperature=0.9,
544
  eos_token_id=terminators,
545
  )
546
 
@@ -561,35 +655,59 @@ class UnifiedAudioConverter:
561
 
562
  except Exception as e:
563
  print(f"Legacy local model also failed: {e}")
564
- # Return default template with Korean male names using formal speech
565
  if language == "Korean":
566
- return {
567
- "conversation": [
568
- {"speaker": "์ค€์ˆ˜", "text": "์•ˆ๋…•ํ•˜์„ธ์š”, ์—ฌ๋Ÿฌ๋ถ„! ์˜ค๋Š˜๋„ ์ €ํฌ ํŒŸ์บ์ŠคํŠธ๋ฅผ ์ฐพ์•„์ฃผ์…”์„œ ์ •๋ง ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๋ฏผํ˜ธ์”จ, ์˜ค๋Š˜ ์ •๋ง ํฅ๋ฏธ๋กœ์šด ์ฃผ์ œ๋ฅผ ์ค€๋น„ํ•ด์ฃผ์…จ๋‹ค๊ณ  ๋“ค์—ˆ์–ด์š”."},
569
- {"speaker": "๋ฏผํ˜ธ", "text": "๋„ค, ์•ˆ๋…•ํ•˜์„ธ์š”! ์ค€์ˆ˜์”จ ๋ง์”€์ฒ˜๋Ÿผ ์˜ค๋Š˜์€ ์ •๋ง ํฅ๋ฏธ๋กœ์šด ์ด์•ผ๊ธฐ๋ฅผ ์ค€๋น„ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฒญ์ทจ์ž ์—ฌ๋Ÿฌ๋ถ„๋“ค๊ป˜์„œ๋„ ๋งŽ์€ ๊ด€์‹ฌ์„ ๊ฐ€์ง€๊ณ  ๊ณ„์‹ค ์ฃผ์ œ์ธ ๊ฒƒ ๊ฐ™์•„์š”."},
570
- {"speaker": "์ค€์ˆ˜", "text": "์ •๋ง ๊ธฐ๋Œ€๋˜๋Š”๋ฐ์š”. ๊ทธ๋Ÿฐ๋ฐ ๋ฏผํ˜ธ์”จ, ์ด ์ฃผ์ œ๊ฐ€ ์š”์ฆ˜ ์™œ ์ด๋ ‡๊ฒŒ ํ™”์ œ๊ฐ€ ๋˜๊ณ  ์žˆ๋Š” ๊ฑด๊ฐ€์š”? ์ฒญ์ทจ์ž ์—ฌ๋Ÿฌ๋ถ„๋“ค๋„ ๊ถ๊ธˆํ•ดํ•˜์‹ค ๊ฒƒ ๊ฐ™์•„์š”."},
571
- {"speaker": "๋ฏผํ˜ธ", "text": "์ข‹์€ ์งˆ๋ฌธ์ด์„ธ์š”, ์ค€์ˆ˜์”จ. ์‚ฌ์‹ค ์ตœ๊ทผ์— ์ด ๋ถ„์•ผ์— ๋งŽ์€ ๋ณ€ํ™”๊ฐ€ ์žˆ์—ˆ๊ฑฐ๋“ ์š”. ๊ทธ๋Ÿผ ๋ณธ๊ฒฉ์ ์œผ๋กœ ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์„ค๋ช…ํ•ด๋“œ๋ฆด๊ฒŒ์š”."}
572
- ]
573
- }
574
  else:
575
- return {
576
- "conversation": [
577
- {"speaker": "Alex", "text": "Welcome everyone to our podcast! We have an fascinating topic to discuss today."},
578
- {"speaker": "Jordan", "text": "Thanks for having me, Alex. I'm excited to dive into this subject with our listeners."},
579
- {"speaker": "Alex", "text": "So let's get started. Can you give us an overview of what we'll be covering?"},
580
- {"speaker": "Jordan", "text": "Absolutely! Today we'll explore some really interesting aspects that I think will surprise many people."}
581
- ]
582
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
 
584
  def extract_conversation_api(self, text: str, language: str = "English") -> Dict:
585
- """Extract conversation using API with search context"""
586
  if not self.llm_client:
587
  raise RuntimeError("API mode not initialized")
588
 
589
  try:
590
  # ๊ฒ€์ƒ‰ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ
591
  search_context = ""
592
- if BRAVE_KEY:
593
  try:
594
  keywords = extract_keywords_for_search(text, language)
595
  if keywords:
@@ -599,21 +717,24 @@ class UnifiedAudioConverter:
599
  except Exception as e:
600
  print(f"Search failed, continuing without context: {e}")
601
 
602
- # ๊ฐ•ํ™”๋œ ์–ธ์–ด๋ณ„ ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ
603
  if language == "Korean":
604
  system_message = (
605
- "๋‹น์‹ ์€ ํ•œ๊ตญ์–ด ํŒŸ์บ์ŠคํŠธ ์ „๋ฌธ ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค. "
606
- "ํ•œ๊ตญ ์ฒญ์ทจ์ž๋“ค์˜ ๋ฌธํ™”์  ๋งฅ๋ฝ๊ณผ ์–ธ์–ด์  ํŠน์„ฑ์„ ์™„๋ฒฝํžˆ ์ดํ•ดํ•˜๊ณ , "
607
- "์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋งค๋ ฅ์ ์ธ ๋Œ€๋ณธ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. "
608
- "์ค€์ˆ˜(์ง„ํ–‰์ž)์™€ ๋ฏผํ˜ธ(์ „๋ฌธ๊ฐ€)๋ผ๋Š” ๋‘ ๋ช…์˜ 30๋Œ€ ๋‚จ์„ฑ์ด ์„œ๋กœ์—๊ฒŒ ์กด๋Œ“๋ง์„ ์‚ฌ์šฉํ•˜์—ฌ ๋Œ€ํ™”ํ•˜๋Š” ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”. "
609
- "๋ฐ˜๋ง์€ ์ ˆ๋Œ€ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉฐ, ์ตœ์‹  ์ •๋ณด๋„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ฐ˜์˜ํ•˜์„ธ์š”."
 
610
  )
611
  else:
612
  system_message = (
613
- "You are an expert podcast scriptwriter who creates engaging, "
614
- "natural conversations between Alex (host) and Jordan (expert). "
615
- "Create informative yet entertaining dialogue that keeps listeners engaged. "
616
- "Incorporate current trends and latest information naturally."
 
 
617
  )
618
 
619
  chat_completion = self.llm_client.chat.completions.create(
@@ -622,6 +743,7 @@ class UnifiedAudioConverter:
622
  {"role": "user", "content": self._build_prompt(text, language, search_context)}
623
  ],
624
  model=self.config.api_model_name,
 
625
  )
626
 
627
  pattern = r"\{(?:[^{}]|(?:\{[^{}]*\}))*\}"
@@ -871,18 +993,23 @@ converter = UnifiedAudioConverter(ConversationConfig())
871
 
872
 
873
  async def synthesize(article_input, input_type: str = "URL", mode: str = "Local", tts_engine: str = "Edge-TTS", language: str = "English"):
874
- """Main synthesis function - handles both URL and PDF inputs"""
875
  try:
876
  # Extract text based on input type
877
  if input_type == "URL":
878
  if not article_input or not isinstance(article_input, str):
879
  return "Please provide a valid URL.", None
880
  text = converter.fetch_text(article_input)
881
- else: # PDF
882
  if not article_input:
883
  return "Please upload a PDF file.", None
884
- # Gradio returns the file path as a string
885
  text = converter.extract_text_from_pdf(article_input)
 
 
 
 
 
 
886
 
887
  # Limit text to max words
888
  words = text.split()
@@ -942,7 +1069,7 @@ async def regenerate_audio(conversation_text: str, tts_engine: str = "Edge-TTS",
942
 
943
  # ํ•œ๊ตญ์–ด์ธ ๊ฒฝ์šฐ Edge-TTS๋งŒ ์‚ฌ์šฉ (๋‹ค๋ฅธ TTS๋Š” ํ•œ๊ตญ์–ด ์ง€์›์ด ์ œํ•œ์ )
944
  if language == "Korean" and tts_engine != "Edge-TTS":
945
- return "ํ•œ๊ตญ์–ด๋Š” Edge-TTS๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค. TTS ์—”์ง„์ด ์ž๋™์œผ๋กœ Edge-TTS๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.", None
946
 
947
  # Generate audio based on TTS engine
948
  if tts_engine == "Edge-TTS":
@@ -997,11 +1124,13 @@ def update_tts_engine_for_korean(language):
997
 
998
 
999
  def toggle_input_visibility(input_type):
1000
- """Toggle visibility of URL input and file upload based on input type"""
1001
  if input_type == "URL":
1002
- return gr.update(visible=True), gr.update(visible=False)
1003
- else: # PDF
1004
- return gr.update(visible=False), gr.update(visible=True)
 
 
1005
 
1006
 
1007
  # ๋ชจ๋ธ ์ดˆ๊ธฐํ™” (์•ฑ ์‹œ์ž‘ ์‹œ)
@@ -1018,29 +1147,30 @@ if LLAMA_CPP_AVAILABLE:
1018
 
1019
 
1020
  # Gradio Interface
1021
- with gr.Blocks(theme='soft', title="URL/PDF to Podcast Converter") as demo:
1022
- gr.Markdown("# ๐ŸŽ™๏ธ URL/PDF to Podcast Converter")
1023
- gr.Markdown("Convert any article, blog, news, or PDF document into an engaging podcast conversation!")
1024
 
1025
  # ์ƒ๋‹จ์— ๋กœ์ปฌ LLM ์ƒํƒœ ํ‘œ์‹œ
1026
  with gr.Row():
1027
  gr.Markdown(f"""
1028
- ### ๐Ÿค– LLM Configuration:
1029
  - **Primary**: Local LLM ({converter.config.local_model_name}) - Runs on your device
1030
  - **Fallback**: API LLM ({converter.config.api_model_name}) - Used when local fails
1031
  - **Status**: {"โœ… Llama CPP Available" if LLAMA_CPP_AVAILABLE else "โŒ Llama CPP Not Available - Install llama-cpp-python"}
1032
- - **Max Tokens**: {converter.config.max_tokens} (Extended for longer conversations)
1033
  - **Search**: {"โœ… Brave Search Enabled" if BRAVE_KEY else "โŒ Brave Search Not Available - Set BSEARCH_API"}
 
1034
  """)
1035
 
1036
  with gr.Row():
1037
  with gr.Column(scale=3):
1038
- # Input type selector
1039
  input_type_selector = gr.Radio(
1040
- choices=["URL", "PDF"],
1041
  value="URL",
1042
  label="Input Type",
1043
- info="Choose between URL or PDF file upload"
1044
  )
1045
 
1046
  # URL input
@@ -1058,8 +1188,17 @@ with gr.Blocks(theme='soft', title="URL/PDF to Podcast Converter") as demo:
1058
  visible=False
1059
  )
1060
 
 
 
 
 
 
 
 
 
 
1061
  with gr.Column(scale=1):
1062
- # ์–ธ์–ด ์„ ํƒ ์ถ”๊ฐ€
1063
  language_selector = gr.Radio(
1064
  choices=["English", "Korean"],
1065
  value="English",
@@ -1085,17 +1224,22 @@ with gr.Blocks(theme='soft', title="URL/PDF to Podcast Converter") as demo:
1085
  )
1086
 
1087
  gr.Markdown("""
1088
- **Recommended:**
1089
- - ๐ŸŒŸ **Edge-TTS**: Best quality, cloud-based, instant setup
1090
- - ๐Ÿค– **Spark-TTS**: Local AI model (0.5B), zero-shot voice cloning
 
 
1091
 
1092
- **Additional Option:**
1093
- - โšก **MeloTTS**: Local processing, GPU recommended
 
 
1094
 
1095
- **ํ•œ๊ตญ์–ด ์ง€์›:**
1096
- - ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด ์„ ํƒ ์‹œ Edge-TTS๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค
1097
- - ๐Ÿ‘จโ€๐Ÿ‘จ ํ•œ๊ตญ์–ด ๋Œ€ํ™”๋Š” ์ค€์ˆ˜(์ง„ํ–‰์ž)์™€ ๋ฏผํ˜ธ(์ „๋ฌธ๊ฐ€) ๋‘ ๋‚จ์„ฑ์ด ์กด๋Œ“๋ง๋กœ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค
1098
- - ๐Ÿ” **์ตœ์‹  ์ •๋ณด ๋ฐ˜์˜**: Brave Search๋ฅผ ํ†ตํ•ด ์ตœ์‹  ์‹œ์‚ฌ ๋‚ด์šฉ์„ ์ž๋™์œผ๋กœ ๊ฒ€์ƒ‰ํ•˜์—ฌ ๋Œ€๋ณธ์— ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค
 
1099
  """)
1100
 
1101
  convert_btn = gr.Button("๐ŸŽฏ Generate Conversation / ๋Œ€ํ™” ์ƒ์„ฑ", variant="primary", size="lg")
@@ -1104,10 +1248,10 @@ with gr.Blocks(theme='soft', title="URL/PDF to Podcast Converter") as demo:
1104
  with gr.Column():
1105
  conversation_output = gr.Textbox(
1106
  label="Generated Conversation (Editable) / ์ƒ์„ฑ๋œ ๋Œ€ํ™” (ํŽธ์ง‘ ๊ฐ€๋Šฅ)",
1107
- lines=25, # ๋” ๊ธด ๋Œ€ํ™”๋ฅผ ์œ„ํ•ด ์ฆ๊ฐ€
1108
- max_lines=50,
1109
  interactive=True,
1110
- placeholder="Generated conversation will appear here. You can edit it before generating audio.\n์ƒ์„ฑ๋œ ๋Œ€ํ™”๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์˜ค๋””์˜ค ์ƒ์„ฑ ์ „์— ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\n\nํ•œ๊ตญ์–ด ๋Œ€ํ™”๋Š” ์กด๋Œ“๋ง๋กœ ์ง„ํ–‰๋˜๋ฉฐ ์ตœ์‹  ์‹œ์‚ฌ ๋‚ด์šฉ์ด ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.",
1111
  info="Edit the conversation as needed. Format: 'Speaker Name: Text' / ํ•„์š”์— ๋”ฐ๋ผ ๋Œ€ํ™”๋ฅผ ํŽธ์ง‘ํ•˜์„ธ์š”. ํ˜•์‹: 'ํ™”์ž ์ด๋ฆ„: ํ…์ŠคํŠธ'"
1112
  )
1113
 
@@ -1134,8 +1278,9 @@ with gr.Blocks(theme='soft', title="URL/PDF to Podcast Converter") as demo:
1134
  gr.Examples(
1135
  examples=[
1136
  ["https://huggingface.co/blog/openfree/cycle-navigator", "URL", "Local", "Edge-TTS", "English"],
1137
- ["https://www.bbc.com/news/technology-67988517", "URL", "Local", "Spark-TTS", "English"],
1138
  ["https://huggingface.co/papers/2505.14810", "URL", "Local", "Edge-TTS", "Korean"],
 
1139
  ],
1140
  inputs=[url_input, input_type_selector, mode_selector, tts_selector, language_selector],
1141
  outputs=[conversation_output, status_output],
@@ -1143,11 +1288,11 @@ with gr.Blocks(theme='soft', title="URL/PDF to Podcast Converter") as demo:
1143
  cache_examples=False,
1144
  )
1145
 
1146
- # Input type change handler
1147
  input_type_selector.change(
1148
  fn=toggle_input_visibility,
1149
  inputs=[input_type_selector],
1150
- outputs=[url_input, pdf_input]
1151
  )
1152
 
1153
  # ์–ธ์–ด ๋ณ€๊ฒฝ ์‹œ TTS ์—”์ง„ ์˜ต์…˜ ์—…๋ฐ์ดํŠธ
@@ -1158,18 +1303,20 @@ with gr.Blocks(theme='soft', title="URL/PDF to Podcast Converter") as demo:
1158
  )
1159
 
1160
  # ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ - ์ˆ˜์ •๋œ ๋ถ€๋ถ„
1161
- def get_article_input(input_type, url_input, pdf_input):
1162
  """Get the appropriate input based on input type"""
1163
  if input_type == "URL":
1164
  return url_input
1165
- else:
1166
  return pdf_input
 
 
1167
 
1168
  convert_btn.click(
1169
- fn=lambda input_type, url_input, pdf_input, mode, tts, lang: synthesize_sync(
1170
- get_article_input(input_type, url_input, pdf_input), input_type, mode, tts, lang
1171
  ),
1172
- inputs=[input_type_selector, url_input, pdf_input, mode_selector, tts_selector, language_selector],
1173
  outputs=[conversation_output, status_output]
1174
  )
1175
 
 
81
 
82
  @dataclass
83
  class ConversationConfig:
84
+ max_words: int = 6000 # 4000์—์„œ 6000์œผ๋กœ ์ฆ๊ฐ€ (1.5๋ฐฐ)
85
  prefix_url: str = "https://r.jina.ai/"
86
  api_model_name: str = "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo"
87
  legacy_local_model_name: str = "NousResearch/Hermes-2-Pro-Llama-3-8B"
 
89
  local_model_name: str = "Private-BitSix-Mistral-Small-3.1-24B-Instruct-2503.gguf"
90
  local_model_repo: str = "ginigen/Private-BitSix-Mistral-Small-3.1-24B-Instruct-2503"
91
  # ํ† ํฐ ์ˆ˜ ์ฆ๊ฐ€
92
+ max_tokens: int = 4500 # 3000์—์„œ 4500์œผ๋กœ ์ฆ๊ฐ€ (1.5๋ฐฐ)
93
+ max_new_tokens: int = 9000 # 6000์—์„œ 9000์œผ๋กœ ์ฆ๊ฐ€ (1.5๋ฐฐ)
94
+ min_conversation_turns: int = 12 # ์ตœ์†Œ ๋Œ€ํ™” ํ„ด ์ˆ˜
95
+ max_conversation_turns: int = 15 # ์ตœ๋Œ€ ๋Œ€ํ™” ํ„ด ์ˆ˜
96
 
97
 
98
  def brave_search(query: str, count: int = 8, freshness_days: int | None = None):
 
121
  logging.error(f"Brave search error: {e}")
122
  return []
123
 
124
+ def format_search_results(query: str, for_keyword: bool = False) -> str:
125
+ """๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ํฌ๋งทํŒ…ํ•˜์—ฌ ๋ฐ˜ํ™˜"""
126
+ # ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์˜ ๊ฒฝ์šฐ ๋” ๋งŽ์€ ๊ฒฐ๊ณผ ์‚ฌ์šฉ
127
+ count = 5 if for_keyword else 3
128
+ rows = brave_search(query, count, freshness_days=7 if not for_keyword else None)
129
  if not rows:
130
+ return ""
131
 
 
132
  results = []
133
+ # ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์˜ ๊ฒฝ์šฐ ๋” ์ƒ์„ธํ•œ ์ •๋ณด ํฌํ•จ
134
+ max_results = 4 if for_keyword else 2
135
+ for r in rows[:max_results]:
136
+ if for_keyword:
137
+ # ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์€ ๋” ๊ธด ์Šค๋‹ˆํŽซ ์‚ฌ์šฉ
138
+ snippet = r['snippet'][:200] + "..." if len(r['snippet']) > 200 else r['snippet']
139
+ results.append(f"**{r['title']}**\n{snippet}\nSource: {r['host']}")
140
+ else:
141
+ # ์ผ๋ฐ˜ ๊ฒ€์ƒ‰์€ ์งง์€ ์Šค๋‹ˆํŽซ
142
+ snippet = r['snippet'][:100] + "..." if len(r['snippet']) > 100 else r['snippet']
143
+ results.append(f"- {r['title']}: {snippet}")
144
 
145
+ return "\n\n".join(results) + "\n"
146
 
147
  def extract_keywords_for_search(text: str, language: str = "English") -> List[str]:
148
  """ํ…์ŠคํŠธ์—์„œ ๊ฒ€์ƒ‰ํ•  ํ‚ค์›Œ๋“œ ์ถ”์ถœ (๊ฐœ์„ )"""
 
167
  return [max(keywords, key=len)] # ๊ฐ€์žฅ ๊ธด ๋‹จ์–ด 1๊ฐœ
168
  return []
169
 
170
+ def search_and_compile_content(keyword: str, language: str = "English") -> str:
171
+ """ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰ํ•˜์—ฌ ์ฝ˜ํ…์ธ  ์ปดํŒŒ์ผ"""
172
+ if not BRAVE_KEY:
173
+ return f"Search API not available. Using keyword: {keyword}"
174
+
175
+ # ์–ธ์–ด์— ๋”ฐ๋ฅธ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ์กฐ์ •
176
+ if language == "Korean":
177
+ queries = [
178
+ f"{keyword} ์ตœ์‹  ๋‰ด์Šค",
179
+ f"{keyword} ์ •๋ณด",
180
+ f"{keyword} ํŠธ๋ Œ๋“œ 2024"
181
+ ]
182
+ else:
183
+ queries = [
184
+ f"{keyword} latest news",
185
+ f"{keyword} explained",
186
+ f"{keyword} trends 2024"
187
+ ]
188
+
189
+ all_content = []
190
+
191
+ for query in queries:
192
+ results = brave_search(query, count=3)
193
+ for r in results[:2]: # ๊ฐ ์ฟผ๋ฆฌ๋‹น ์ƒ์œ„ 2๊ฐœ ๊ฒฐ๊ณผ
194
+ content = f"**{r['title']}**\n{r['snippet']}\n"
195
+ all_content.append(content)
196
+
197
+ if not all_content:
198
+ return f"No search results found for: {keyword}"
199
+
200
+ # ์ปดํŒŒ์ผ๋œ ์ฝ˜ํ…์ธ  ๋ฐ˜ํ™˜
201
+ compiled = "\n\n".join(all_content)
202
+
203
+ # ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ์†Œ๊ฐœ ์ถ”๊ฐ€
204
+ if language == "Korean":
205
+ intro = f"'{keyword}'์— ๋Œ€ํ•œ ์ตœ์‹  ์ •๋ณด์™€ ํŠธ๋ Œ๋“œ:\n\n"
206
+ else:
207
+ intro = f"Latest information and trends about '{keyword}':\n\n"
208
+
209
+ return intro + compiled
210
+
211
  class UnifiedAudioConverter:
212
  def __init__(self, config: ConversationConfig):
213
  self.config = config
 
361
 
362
 
363
  def _build_prompt(self, text: str, language: str = "English", search_context: str = "") -> str:
364
+ """Build prompt for conversation generation with enhanced radio talk show style"""
365
+ # ํ…์ŠคํŠธ ๊ธธ์ด ์ œํ•œ
366
+ max_text_length = 4500 if search_context else 6000
367
  if len(text) > max_text_length:
368
  text = text[:max_text_length] + "..."
369
 
370
  if language == "Korean":
371
+ # ๋Œ€ํ™” ํ…œํ”Œ๋ฆฟ์„ ๋” ๋งŽ์€ ํ„ด์œผ๋กœ ํ™•์žฅ
372
  template = """
373
  {
374
  "conversation": [
375
+ {"speaker": "์ค€์ˆ˜", "text": ""},
376
+ {"speaker": "๋ฏผํ˜ธ", "text": ""},
377
+ {"speaker": "์ค€์ˆ˜", "text": ""},
378
+ {"speaker": "๋ฏผํ˜ธ", "text": ""},
379
+ {"speaker": "์ค€์ˆ˜", "text": ""},
380
+ {"speaker": "๋ฏผํ˜ธ", "text": ""},
381
+ {"speaker": "์ค€์ˆ˜", "text": ""},
382
+ {"speaker": "๋ฏผํ˜ธ", "text": ""},
383
  {"speaker": "์ค€์ˆ˜", "text": ""},
384
  {"speaker": "๋ฏผํ˜ธ", "text": ""},
385
  {"speaker": "์ค€์ˆ˜", "text": ""},
 
388
  }
389
  """
390
 
 
391
  context_part = ""
392
  if search_context:
393
  context_part = f"# ์ตœ์‹  ๊ด€๋ จ ์ •๋ณด:\n{search_context}\n"
 
395
  base_prompt = (
396
  f"# ์›๋ณธ ์ฝ˜ํ…์ธ :\n{text}\n\n"
397
  f"{context_part}"
398
+ f"์œ„ ๋‚ด์šฉ์œผ๋กœ ๋ผ๋””์˜ค ๋Œ€๋‹ด ํ”„๋กœ๊ทธ๋žจ ๋Œ€๋ณธ์„ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”.\n\n"
399
  f"## ํ•ต์‹ฌ ์ง€์นจ:\n"
400
+ f"1. **๋Œ€ํ™” ์Šคํƒ€์ผ**: ์‹ค์ œ ๋ผ๋””์˜ค ๋Œ€๋‹ด์ฒ˜๋Ÿผ ์•„์ฃผ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ํŽธ์•ˆํ•œ ๊ตฌ์–ด์ฒด ์‚ฌ์šฉ\n"
401
+ f"2. **ํ™”์ž ์—ญํ• **:\n"
402
+ f" - ์ค€์ˆ˜: ์ง„ํ–‰์ž/ํ˜ธ์ŠคํŠธ (์ฃผ๋กœ ์งˆ๋ฌธํ•˜๊ณ  ๋Œ€ํ™”๋ฅผ ์ด๋Œ์–ด๊ฐ)\n"
403
+ f" - ๋ฏผํ˜ธ: ์ „๋ฌธ๊ฐ€ (์งˆ๋ฌธ์— ๋‹ตํ•˜๊ณ  ์„ค๋ช…ํ•จ)\n"
404
+ f"3. **๋Œ€ํ™” ํŒจํ„ด**:\n"
405
+ f" - ์ค€์ˆ˜๋Š” ์ฃผ๋กœ ์งง์€ ์งˆ๋ฌธ์ด๋‚˜ ๋ฆฌ์•ก์…˜ (\"์•„, ๊ทธ๋ ‡๊ตฐ์š”\", \"ํฅ๋ฏธ๋กญ๋„ค์š”\", \"๊ทธ๋Ÿผ ~๋Š” ์–ด๋–ค๊ฐ€์š”?\")\n"
406
+ f" - ๋ฏผํ˜ธ๋Š” 1-2๋ฌธ์žฅ์œผ๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋‹ต๋ณ€\n"
407
+ f" - ์ ˆ๋Œ€ ํ•œ ์‚ฌ๋žŒ์ด 3๋ฌธ์žฅ ์ด์ƒ ์—ฐ์†์œผ๋กœ ๋งํ•˜์ง€ ์•Š์Œ\n"
408
+ f"4. **์ž์—ฐ์Šค๋Ÿฌ์›€**:\n"
409
+ f" - \"์Œ...\", \"์•„...\", \"๋„ค,\" ๊ฐ™์€ ์ถ”์ž„์ƒˆ ์‚ฌ์šฉ\n"
410
+ f" - ๋•Œ๋กœ๋Š” ์ƒ๋Œ€๋ฐฉ ๋ง์— ์งง๊ฒŒ ๋ฐ˜์‘ (\"๋งž์•„์š”\", \"๊ทธ๋ ‡์ฃ \")\n"
411
+ f"5. **ํ•„์ˆ˜ ๊ทœ์น™**: ์„œ๋กœ ์กด๋Œ“๋ง ์‚ฌ์šฉ, 12-15ํšŒ ๋Œ€ํ™” ๊ตํ™˜\n\n"
412
  f"JSON ํ˜•์‹์œผ๋กœ๋งŒ ๋ฐ˜ํ™˜:\n{template}"
413
  )
414
 
415
  return base_prompt
416
 
417
  else:
418
+ # ์˜์–ด ํ…œํ”Œ๋ฆฟ๋„ ํ™•์žฅ
419
  template = """
420
  {
421
  "conversation": [
422
+ {"speaker": "Alex", "text": ""},
423
+ {"speaker": "Jordan", "text": ""},
424
+ {"speaker": "Alex", "text": ""},
425
+ {"speaker": "Jordan", "text": ""},
426
+ {"speaker": "Alex", "text": ""},
427
+ {"speaker": "Jordan", "text": ""},
428
+ {"speaker": "Alex", "text": ""},
429
+ {"speaker": "Jordan", "text": ""},
430
  {"speaker": "Alex", "text": ""},
431
  {"speaker": "Jordan", "text": ""},
432
  {"speaker": "Alex", "text": ""},
 
442
  base_prompt = (
443
  f"# Content:\n{text}\n\n"
444
  f"{context_part}"
445
+ f"Create a natural radio talk show conversation.\n\n"
446
+ f"## Key Guidelines:\n"
447
+ f"1. **Style**: Natural, conversational English like a real radio show\n"
448
+ f"2. **Roles**:\n"
449
+ f" - Alex: Host (asks questions, guides conversation)\n"
450
+ f" - Jordan: Expert (answers, explains)\n"
451
+ f"3. **Pattern**:\n"
452
+ f" - Alex mostly asks short questions or reacts (\"I see\", \"Interesting\", \"What about...?\")\n"
453
+ f" - Jordan gives brief 1-2 sentence answers\n"
454
+ f" - Never more than 2-3 sentences per turn\n"
455
+ f"4. **Natural flow**:\n"
456
+ f" - Use fillers like \"Well,\" \"You know,\" \"Actually,\"\n"
457
+ f" - Short reactions (\"Right\", \"Exactly\")\n"
458
+ f"5. **Length**: 12-15 exchanges total\n\n"
459
  f"Return JSON only:\n{template}"
460
  )
461
 
 
464
 
465
 
466
  def _build_messages_for_local(self, text: str, language: str = "English", search_context: str = "") -> List[Dict]:
467
+ """Build messages for local LLM with enhanced radio talk show style"""
468
  if language == "Korean":
469
  system_message = (
470
+ "๋‹น์‹ ์€ ํ•œ๊ตญ ์ตœ๊ณ ์˜ ๋ผ๋””์˜ค ๋Œ€๋‹ด ํ”„๋กœ๊ทธ๋žจ ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค. "
471
+ "์‹ค์ œ ๋ผ๋””์˜ค ๋ฐฉ์†ก์ฒ˜๋Ÿผ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ์ƒ๋™๊ฐ ์žˆ๋Š” ๋Œ€ํ™”๋ฅผ ๋งŒ๋“ค์–ด๋ƒ…๋‹ˆ๋‹ค.\n\n"
 
472
  "ํ•ต์‹ฌ ์›์น™:\n"
473
+ "1. ๋ผ๋””์˜ค ์ง„ํ–‰์ž(์ค€์ˆ˜)๋Š” ์ฃผ๋กœ ์งง์€ ์งˆ๋ฌธ๊ณผ ๋ฆฌ์•ก์…˜์œผ๋กœ ๋Œ€ํ™”๋ฅผ ์ด๋Œ์–ด๊ฐ‘๋‹ˆ๋‹ค\n"
474
+ "2. ์ „๋ฌธ๊ฐ€(๋ฏผํ˜ธ)๋Š” ์งˆ๋ฌธ์— ๊ฐ„๊ฒฐํ•˜๊ณ  ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋‹ตํ•ฉ๋‹ˆ๋‹ค\n"
475
+ "3. ํ•œ ๋ฒˆ์— ๋„ˆ๋ฌด ๋งŽ์€ ์ •๋ณด๋ฅผ ์ „๋‹ฌํ•˜์ง€ ์•Š๊ณ , ๋Œ€ํ™”๋ฅผ ํ†ตํ•ด ์ ์ง„์ ์œผ๋กœ ํ’€์–ด๊ฐ‘๋‹ˆ๋‹ค\n"
476
+ "4. \"์Œ...\", \"์•„...\", \"๋„ค,\" ๋“ฑ ์ž์—ฐ์Šค๋Ÿฌ์šด ๊ตฌ์–ด์ฒด ํ‘œํ˜„์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค\n"
477
+ "5. ์ฒญ์ทจ์ž๊ฐ€ ๋ผ๋””์˜ค๋ฅผ ๋“ฃ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ชฐ์ž…ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ƒ์ƒํ•˜๊ฒŒ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค\n"
478
+ "6. ๋ฐ˜๋“œ์‹œ ์„œ๋กœ ์กด๋Œ“๋ง์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์ •์ค‘ํ•˜๋ฉด์„œ๋„ ์นœ๊ทผํ•œ ํ†ค์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค"
479
  )
480
  else:
481
  system_message = (
482
+ "You are an expert radio talk show scriptwriter who creates engaging, "
483
+ "natural conversations that sound like real radio broadcasts.\n\n"
484
+ "Key principles:\n"
485
+ "1. The host (Alex) mainly asks short questions and gives reactions to guide the conversation\n"
486
+ "2. The expert (Jordan) answers concisely and clearly\n"
487
+ "3. Information is revealed gradually through dialogue, not in long monologues\n"
488
+ "4. Use natural speech patterns with fillers like 'Well,' 'You know,' etc.\n"
489
+ "5. Make it sound like an actual radio show that listeners would enjoy\n"
490
+ "6. Keep each turn brief - no more than 2-3 sentences"
491
  )
492
 
493
  return [
 
497
 
498
  @spaces.GPU(duration=120)
499
  def extract_conversation_local(self, text: str, language: str = "English", progress=None) -> Dict:
500
+ """Extract conversation using new local LLM with enhanced search and style"""
501
  try:
502
+ # ๊ฒ€์ƒ‰ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ (ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜์ด ์•„๋‹Œ ๊ฒฝ์šฐ)
503
  search_context = ""
504
+ if BRAVE_KEY and not text.startswith("Keyword-based content:"):
505
  try:
506
  keywords = extract_keywords_for_search(text, language)
507
  if keywords:
 
508
  search_query = keywords[0] if language == "Korean" else f"{keywords[0]} latest news"
509
  search_context = format_search_results(search_query)
510
  print(f"Search context added for: {search_query}")
 
517
  chat_template = self._get_messages_formatter_type(self.config.local_model_name)
518
  provider = LlamaCppPythonProvider(self.local_llm)
519
 
520
+ # ๊ฐ•ํ™”๋œ ๋ผ๋””์˜ค ์Šคํƒ€์ผ ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€
521
  if language == "Korean":
522
  system_message = (
523
+ "๋‹น์‹ ์€ ํ•œ๊ตญ์˜ ์ธ๊ธฐ ๋ผ๋””์˜ค ๋Œ€๋‹ด ํ”„๋กœ๊ทธ๋žจ ์ „๋ฌธ ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค. "
524
+ "์ฒญ์ทจ์ž๋“ค์ด ์‹ค์ œ ๋ผ๋””์˜ค๋ฅผ ๋“ฃ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ชฐ์ž…ํ•  ์ˆ˜ ์žˆ๋Š” ์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€ํ™”๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.\n\n"
525
+ "์ž‘์„ฑ ๊ทœ์น™:\n"
526
+ "1. ์ง„ํ–‰์ž(์ค€์ˆ˜)๋Š” ์ฃผ๋กœ ์งง์€ ์งˆ๋ฌธ์œผ๋กœ ๋Œ€ํ™”๋ฅผ ์ด๋Œ์–ด๊ฐ€์„ธ์š” (\"๊ทธ๋ ‡๊ตฐ์š”\", \"์–ด๋–ค ์ ์ด ํŠน๋ณ„ํ•œ๊ฐ€์š”?\", \"์ฒญ์ทจ์ž๋ถ„๋“ค์ด ๊ถ๊ธˆํ•ดํ•˜์‹ค ๊ฒƒ ๊ฐ™์€๋ฐ์š”\")\n"
527
+ "2. ์ „๋ฌธ๊ฐ€(๋ฏผํ˜ธ)๋Š” 1-2๋ฌธ์žฅ์œผ๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋‹ต๋ณ€ํ•˜์„ธ์š”\n"
528
+ "3. ์ ˆ๋Œ€ ํ•œ ์‚ฌ๋žŒ์ด 3๋ฌธ์žฅ ์ด์ƒ ์—ฐ์†์œผ๋กœ ๋งํ•˜์ง€ ๋งˆ์„ธ์š”\n"
529
+ "4. ๊ตฌ์–ด์ฒด์™€ ์ถ”์ž„์ƒˆ๋ฅผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์‚ฌ์šฉํ•˜์„ธ์š”\n"
530
+ "5. ๋ฐ˜๋“œ์‹œ ์„œ๋กœ ์กด๋Œ“๋ง์„ ์‚ฌ์šฉํ•˜์„ธ์š”\n"
531
+ "6. 12-15ํšŒ์˜ ๋Œ€ํ™” ๊ตํ™˜์œผ๋กœ ๊ตฌ์„ฑํ•˜์„ธ์š”\n"
532
+ "7. JSON ํ˜•์‹์œผ๋กœ๋งŒ ์‘๋‹ตํ•˜์„ธ์š”"
533
  )
534
  else:
535
  system_message = (
536
+ "You are a professional radio talk show scriptwriter creating engaging, "
537
+ "natural conversations that sound like real radio broadcasts.\n\n"
538
+ "Writing rules:\n"
539
+ "1. Host (Alex) mainly asks short questions to guide the conversation (\"I see\", \"What makes it special?\", \"Our listeners might wonder...\")\n"
540
+ "2. Expert (Jordan) answers in 1-2 concise sentences\n"
541
+ "3. Never have one person speak more than 2-3 sentences at once\n"
542
+ "4. Use natural speech patterns and fillers\n"
543
+ "5. Create 12-15 conversation exchanges\n"
544
+ "6. Respond only in JSON format"
545
  )
546
 
547
  agent = LlamaCppAgent(
 
552
  )
553
 
554
  settings = provider.get_provider_default_settings()
555
+ settings.temperature = 0.85 # ์•ฝ๊ฐ„ ๋†’์—ฌ์„œ ๋” ์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€ํ™” ์ƒ์„ฑ
556
  settings.top_k = 40
557
  settings.top_p = 0.95
558
  settings.max_tokens = self.config.max_tokens # ์ฆ๊ฐ€๋œ ํ† ํฐ ์ˆ˜ ์‚ฌ์šฉ
 
575
  json_match = re.search(pattern, response)
576
 
577
  if json_match:
578
+ conversation_data = json.loads(json_match.group())
579
+ # ๋Œ€ํ™” ๊ธธ์ด ํ™•์ธ ๋ฐ ์กฐ์ •
580
+ if len(conversation_data["conversation"]) < self.config.min_conversation_turns:
581
+ print(f"Conversation too short ({len(conversation_data['conversation'])} turns), regenerating...")
582
+ # ์žฌ์‹œ๋„ ๋กœ์ง ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
583
+ return conversation_data
584
  else:
585
  raise ValueError("No valid JSON found in local LLM response")
586
 
 
590
 
591
  @spaces.GPU(duration=120)
592
  def extract_conversation_legacy_local(self, text: str, language: str = "English", progress=None, search_context: str = "") -> Dict:
593
+ """Extract conversation using legacy local model with enhanced style"""
594
  try:
595
  self.initialize_legacy_local_mode()
596
 
597
+ # ๊ฐ•ํ™”๋œ ๋ผ๋””์˜ค ์Šคํƒ€์ผ ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€
598
  if language == "Korean":
599
  system_message = (
600
+ "๋‹น์‹ ์€ ๋ผ๋””์˜ค ๋Œ€๋‹ด ํ”„๋กœ๊ทธ๋žจ ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค. "
601
+ "์ง„ํ–‰์ž(์ค€์ˆ˜)๋Š” ์งง์€ ์งˆ๋ฌธ์œผ๋กœ, ์ „๋ฌธ๊ฐ€(๋ฏผํ˜ธ)๋Š” ๊ฐ„๊ฒฐํ•œ ๋‹ต๋ณ€์œผ๋กœ "
602
+ "์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€ํ™”๋ฅผ ๋งŒ๋“œ์„ธ์š”. ์„œ๋กœ ์กด๋Œ“๋ง์„ ์‚ฌ์šฉํ•˜๊ณ , "
603
+ "ํ•œ ๋ฒˆ์— 2-3๋ฌธ์žฅ ์ด๋‚ด๋กœ ๋งํ•˜์„ธ์š”. 12-15ํšŒ ๋Œ€ํ™” ๊ตํ™˜์œผ๋กœ ๊ตฌ์„ฑํ•˜์„ธ์š”."
 
604
  )
605
  else:
606
  system_message = (
607
+ "You are a radio talk show scriptwriter. "
608
+ "Create natural dialogue where the host (Alex) asks short questions "
609
+ "and the expert (Jordan) gives brief answers. "
610
+ "Keep each turn to 2-3 sentences max. Create 12-15 exchanges."
611
  )
612
 
613
  chat = [
 
634
  streamer=streamer,
635
  max_new_tokens=self.config.max_new_tokens, # ์ฆ๊ฐ€๋œ ํ† ํฐ ์ˆ˜ ์‚ฌ์šฉ
636
  do_sample=True,
637
+ temperature=0.85,
638
  eos_token_id=terminators,
639
  )
640
 
 
655
 
656
  except Exception as e:
657
  print(f"Legacy local model also failed: {e}")
658
+ # Return enhanced default template
659
  if language == "Korean":
660
+ return self._get_default_korean_conversation()
 
 
 
 
 
 
 
661
  else:
662
+ return self._get_default_english_conversation()
663
+
664
+ def _get_default_korean_conversation(self) -> Dict:
665
+ """๋” ๊ธด ๊ธฐ๋ณธ ํ•œ๊ตญ์–ด ๋Œ€ํ™” ํ…œํ”Œ๋ฆฟ"""
666
+ return {
667
+ "conversation": [
668
+ {"speaker": "์ค€์ˆ˜", "text": "์•ˆ๋…•ํ•˜์„ธ์š”, ์—ฌ๋Ÿฌ๋ถ„! ์˜ค๋Š˜๋„ ์ €ํฌ ํŒŸ์บ์ŠคํŠธ๋ฅผ ์ฐพ์•„์ฃผ์…”์„œ ์ •๋ง ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค."},
669
+ {"speaker": "๋ฏผํ˜ธ", "text": "๋„ค, ์•ˆ๋…•ํ•˜์„ธ์š”! ์˜ค๋Š˜ ์ •๋ง ํฅ๋ฏธ๋กœ์šด ์ฃผ์ œ๋ฅผ ์ค€๋น„ํ–ˆ์Šต๋‹ˆ๋‹ค."},
670
+ {"speaker": "์ค€์ˆ˜", "text": "์•„, ๊ทธ๋ž˜์š”? ์–ด๋–ค ๋‚ด์šฉ์ธ์ง€ ์ •๋ง ๊ถ๊ธˆํ•œ๋ฐ์š”?"},
671
+ {"speaker": "๋ฏผํ˜ธ", "text": "์˜ค๋Š˜์€ ์ตœ๊ทผ ๋งŽ์€ ๋ถ„๋“ค์ด ๊ด€์‹ฌ์„ ๊ฐ€์ง€๊ณ  ๊ณ„์‹  ์ฃผ์ œ์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊นŒ ํ•ด์š”."},
672
+ {"speaker": "์ค€์ˆ˜", "text": "์Œ, ์š”์ฆ˜ ์ •๋ง ํ™”์ œ๊ฐ€ ๋˜๊ณ  ์žˆ์ฃ . ๊ตฌ์ฒด์ ์œผ๋กœ ์–ด๋–ค ์ธก๋ฉด์„ ๋‹ค๋ฃฐ ์˜ˆ์ •์ด์‹ ๊ฐ€์š”?"},
673
+ {"speaker": "๋ฏผํ˜ธ", "text": "๋„ค, ๋จผ์ € ๊ธฐ๋ณธ์ ์ธ ๊ฐœ๋…๋ถ€ํ„ฐ ์ฐจ๊ทผ์ฐจ๊ทผ ์„ค๋ช…๋“œ๋ฆฌ๊ณ , ์‹ค์ƒํ™œ์— ์–ด๋–ป๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์•Œ์•„๋ณผ๊ฒŒ์š”."},
674
+ {"speaker": "์ค€์ˆ˜", "text": "์ข‹์•„์š”! ์ฒญ์ทจ์ž๋ถ„๋“ค๋„ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•ด์ฃผ์‹ค ๊ฑฐ์ฃ ?"},
675
+ {"speaker": "๋ฏผํ˜ธ", "text": "๋ฌผ๋ก ์ด์ฃ . ์ตœ๋Œ€ํ•œ ์‰ฝ๊ณ  ์žฌ๋ฏธ์žˆ๊ฒŒ ํ’€์–ด์„œ ์„ค๋ช…๋“œ๋ฆด๊ฒŒ์š”."},
676
+ {"speaker": "์ค€์ˆ˜", "text": "๊ทธ๋Ÿผ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ์š”?"},
677
+ {"speaker": "๋ฏผํ˜ธ", "text": "๋„ค, ์ข‹์Šต๋‹ˆ๋‹ค. ์šฐ์„  ์ด ์ฃผ์ œ๊ฐ€ ์™œ ์ค‘์š”ํ•œ์ง€๋ถ€ํ„ฐ ๋ง์”€๋“œ๋ฆด๊ฒŒ์š”."},
678
+ {"speaker": "์ค€์ˆ˜", "text": "์•„, ๋งž์•„์š”. ๊ทธ ๋ถ€๋ถ„์ด ์ •๋ง ์ค‘์š”ํ•˜์ฃ ."},
679
+ {"speaker": "๋ฏผํ˜ธ", "text": "์ตœ๊ทผ ์—ฐ๊ตฌ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด ์ •๋ง ๋†€๋ผ์šด ๋ฐœ๊ฒฌ๋“ค์ด ๋งŽ์•˜์–ด์š”."}
680
+ ]
681
+ }
682
+
683
+ def _get_default_english_conversation(self) -> Dict:
684
+ """Enhanced default English conversation template"""
685
+ return {
686
+ "conversation": [
687
+ {"speaker": "Alex", "text": "Welcome everyone to our podcast! We have a fascinating topic today."},
688
+ {"speaker": "Jordan", "text": "Thanks, Alex. I'm excited to dive into this subject with our listeners."},
689
+ {"speaker": "Alex", "text": "So, what makes this topic particularly relevant right now?"},
690
+ {"speaker": "Jordan", "text": "Well, there have been some significant developments recently that everyone should know about."},
691
+ {"speaker": "Alex", "text": "Interesting! Can you break it down for us?"},
692
+ {"speaker": "Jordan", "text": "Absolutely. Let me start with the basics and build from there."},
693
+ {"speaker": "Alex", "text": "That sounds perfect. Our listeners will appreciate that approach."},
694
+ {"speaker": "Jordan", "text": "So, first, let's understand what we're really talking about here."},
695
+ {"speaker": "Alex", "text": "Right, the fundamentals are crucial."},
696
+ {"speaker": "Jordan", "text": "Exactly. And once we grasp that, the rest becomes much clearer."},
697
+ {"speaker": "Alex", "text": "I'm already learning something new! What's next?"},
698
+ {"speaker": "Jordan", "text": "Now, here's where it gets really interesting..."}
699
+ ]
700
+ }
701
 
702
  def extract_conversation_api(self, text: str, language: str = "English") -> Dict:
703
+ """Extract conversation using API with enhanced radio style"""
704
  if not self.llm_client:
705
  raise RuntimeError("API mode not initialized")
706
 
707
  try:
708
  # ๊ฒ€์ƒ‰ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ
709
  search_context = ""
710
+ if BRAVE_KEY and not text.startswith("Keyword-based content:"):
711
  try:
712
  keywords = extract_keywords_for_search(text, language)
713
  if keywords:
 
717
  except Exception as e:
718
  print(f"Search failed, continuing without context: {e}")
719
 
720
+ # ๊ฐ•ํ™”๋œ ๋ผ๋””์˜ค ์Šคํƒ€์ผ ํ”„๋กฌํ”„ํŠธ
721
  if language == "Korean":
722
  system_message = (
723
+ "๋‹น์‹ ์€ ํ•œ๊ตญ์˜ ์ธ๊ธฐ ๋ผ๋””์˜ค ๋Œ€๋‹ด ํ”„๋กœ๊ทธ๋žจ ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค. "
724
+ "์‹ค์ œ ๋ผ๋””์˜ค ๋ฐฉ์†ก์ฒ˜๋Ÿผ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ํŽธ์•ˆํ•œ ๋Œ€ํ™”๋ฅผ ๋งŒ๋“œ์„ธ์š”.\n"
725
+ "์ค€์ˆ˜(์ง„ํ–‰์ž)๋Š” ์ฃผ๋กœ ์งง์€ ์งˆ๋ฌธ๊ณผ ๋ฆฌ์•ก์…˜์œผ๋กœ ๋Œ€ํ™”๋ฅผ ์ด๋Œ๊ณ , "
726
+ "๋ฏผํ˜ธ(์ „๋ฌธ๊ฐ€)๋Š” 1-2๋ฌธ์žฅ์œผ๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋‹ต๋ณ€ํ•ฉ๋‹ˆ๋‹ค. "
727
+ "๊ตฌ์–ด์ฒด์™€ ์ถ”์ž„์ƒˆ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ๋ฐ˜๋“œ์‹œ ์„œ๋กœ ์กด๋Œ“๋ง์„ ์‚ฌ์šฉํ•˜์„ธ์š”. "
728
+ "12-15ํšŒ์˜ ๋Œ€ํ™” ๊ตํ™˜์œผ๋กœ ๊ตฌ์„ฑํ•˜์„ธ์š”."
729
  )
730
  else:
731
  system_message = (
732
+ "You are a professional radio talk show scriptwriter. "
733
+ "Create natural, engaging dialogue like a real radio broadcast. "
734
+ "Alex (host) mainly asks short questions and gives reactions, "
735
+ "while Jordan (expert) answers in 1-2 concise sentences. "
736
+ "Use conversational language with natural fillers. "
737
+ "Create 12-15 conversation exchanges."
738
  )
739
 
740
  chat_completion = self.llm_client.chat.completions.create(
 
743
  {"role": "user", "content": self._build_prompt(text, language, search_context)}
744
  ],
745
  model=self.config.api_model_name,
746
+ temperature=0.85,
747
  )
748
 
749
  pattern = r"\{(?:[^{}]|(?:\{[^{}]*\}))*\}"
 
993
 
994
 
995
  async def synthesize(article_input, input_type: str = "URL", mode: str = "Local", tts_engine: str = "Edge-TTS", language: str = "English"):
996
+ """Main synthesis function - handles URL, PDF, and Keyword inputs"""
997
  try:
998
  # Extract text based on input type
999
  if input_type == "URL":
1000
  if not article_input or not isinstance(article_input, str):
1001
  return "Please provide a valid URL.", None
1002
  text = converter.fetch_text(article_input)
1003
+ elif input_type == "PDF":
1004
  if not article_input:
1005
  return "Please upload a PDF file.", None
 
1006
  text = converter.extract_text_from_pdf(article_input)
1007
+ else: # Keyword
1008
+ if not article_input or not isinstance(article_input, str):
1009
+ return "Please provide a keyword or topic.", None
1010
+ # ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰ํ•˜์—ฌ ์ฝ˜ํ…์ธ  ์ƒ์„ฑ
1011
+ text = search_and_compile_content(article_input, language)
1012
+ text = f"Keyword-based content:\n{text}" # ๋งˆ์ปค ์ถ”๊ฐ€
1013
 
1014
  # Limit text to max words
1015
  words = text.split()
 
1069
 
1070
  # ํ•œ๊ตญ์–ด์ธ ๊ฒฝ์šฐ Edge-TTS๋งŒ ์‚ฌ์šฉ (๋‹ค๋ฅธ TTS๋Š” ํ•œ๊ตญ์–ด ์ง€์›์ด ์ œํ•œ์ )
1071
  if language == "Korean" and tts_engine != "Edge-TTS":
1072
+ tts_engine = "Edge-TTS" # ์ž๋™์œผ๋กœ Edge-TTS๋กœ ๋ณ€๊ฒฝ
1073
 
1074
  # Generate audio based on TTS engine
1075
  if tts_engine == "Edge-TTS":
 
1124
 
1125
 
1126
  def toggle_input_visibility(input_type):
1127
+ """Toggle visibility of URL input, file upload, and keyword input based on input type"""
1128
  if input_type == "URL":
1129
+ return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
1130
+ elif input_type == "PDF":
1131
+ return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
1132
+ else: # Keyword
1133
+ return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
1134
 
1135
 
1136
  # ๋ชจ๋ธ ์ดˆ๊ธฐํ™” (์•ฑ ์‹œ์ž‘ ์‹œ)
 
1147
 
1148
 
1149
  # Gradio Interface
1150
+ with gr.Blocks(theme='soft', title="AI Podcast Generator") as demo:
1151
+ gr.Markdown("# ๐ŸŽ™๏ธ AI Podcast Generator")
1152
+ gr.Markdown("Convert any article, blog, PDF document, or topic into an engaging podcast conversation!")
1153
 
1154
  # ์ƒ๋‹จ์— ๋กœ์ปฌ LLM ์ƒํƒœ ํ‘œ์‹œ
1155
  with gr.Row():
1156
  gr.Markdown(f"""
1157
+ ### ๐Ÿค– Enhanced Configuration:
1158
  - **Primary**: Local LLM ({converter.config.local_model_name}) - Runs on your device
1159
  - **Fallback**: API LLM ({converter.config.api_model_name}) - Used when local fails
1160
  - **Status**: {"โœ… Llama CPP Available" if LLAMA_CPP_AVAILABLE else "โŒ Llama CPP Not Available - Install llama-cpp-python"}
1161
+ - **Conversation Length**: {converter.config.min_conversation_turns}-{converter.config.max_conversation_turns} exchanges (1.5x longer)
1162
  - **Search**: {"โœ… Brave Search Enabled" if BRAVE_KEY else "โŒ Brave Search Not Available - Set BSEARCH_API"}
1163
+ - **New**: ๐ŸŽฏ Keyword input for topic-based podcast generation
1164
  """)
1165
 
1166
  with gr.Row():
1167
  with gr.Column(scale=3):
1168
+ # Input type selector - ํ‚ค์›Œ๋“œ ์˜ต์…˜ ์ถ”๊ฐ€
1169
  input_type_selector = gr.Radio(
1170
+ choices=["URL", "PDF", "Keyword"],
1171
  value="URL",
1172
  label="Input Type",
1173
+ info="Choose between URL, PDF file upload, or keyword/topic"
1174
  )
1175
 
1176
  # URL input
 
1188
  visible=False
1189
  )
1190
 
1191
+ # Keyword input (์ƒˆ๋กœ ์ถ”๊ฐ€)
1192
+ keyword_input = gr.Textbox(
1193
+ label="Topic/Keyword",
1194
+ placeholder="Enter a topic or keyword (e.g., 'AI trends', '์ธ๊ณต์ง€๋Šฅ ์ตœ์‹  ๋™ํ–ฅ')",
1195
+ value="",
1196
+ visible=False,
1197
+ info="The system will search for latest information about this topic"
1198
+ )
1199
+
1200
  with gr.Column(scale=1):
1201
+ # ์–ธ์–ด ์„ ํƒ
1202
  language_selector = gr.Radio(
1203
  choices=["English", "Korean"],
1204
  value="English",
 
1224
  )
1225
 
1226
  gr.Markdown("""
1227
+ **๐Ÿ“ป Radio Talk Show Style:**
1228
+ - Natural, conversational dialogue
1229
+ - Host asks short questions
1230
+ - Expert gives brief, clear answers
1231
+ - 12-15 conversation exchanges
1232
 
1233
+ **๐Ÿ” Keyword Feature:**
1234
+ - Enter any topic to generate a podcast
1235
+ - Automatically searches latest information
1236
+ - Creates engaging discussion from search results
1237
 
1238
+ **๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด ์ง€์›:**
1239
+ - ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ผ๋””์˜ค ๋Œ€๋‹ด ์Šคํƒ€์ผ
1240
+ - ์ง„ํ–‰์ž(์ค€์ˆ˜)๊ฐ€ ์งง์€ ์งˆ๋ฌธ์œผ๋กœ ๋Œ€ํ™” ์œ ๋„
1241
+ - ์ „๋ฌธ๊ฐ€(๋ฏผํ˜ธ)๊ฐ€ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋‹ต๋ณ€
1242
+ - ์ตœ์‹  ์ •๋ณด ์ž๋™ ๊ฒ€์ƒ‰ ๋ฐ ๋ฐ˜์˜
1243
  """)
1244
 
1245
  convert_btn = gr.Button("๐ŸŽฏ Generate Conversation / ๋Œ€ํ™” ์ƒ์„ฑ", variant="primary", size="lg")
 
1248
  with gr.Column():
1249
  conversation_output = gr.Textbox(
1250
  label="Generated Conversation (Editable) / ์ƒ์„ฑ๋œ ๋Œ€ํ™” (ํŽธ์ง‘ ๊ฐ€๋Šฅ)",
1251
+ lines=30, # ๋” ๊ธด ๋Œ€ํ™”๋ฅผ ์œ„ํ•ด ์ฆ๊ฐ€
1252
+ max_lines=60,
1253
  interactive=True,
1254
+ placeholder="Generated conversation will appear here. You can edit it before generating audio.\n์ƒ์„ฑ๋œ ๋Œ€ํ™”๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์˜ค๋””์˜ค ์ƒ์„ฑ ์ „์— ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\n\n๋ผ๋””์˜ค ๋Œ€๋‹ด ์Šคํƒ€์ผ๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค.",
1255
  info="Edit the conversation as needed. Format: 'Speaker Name: Text' / ํ•„์š”์— ๋”ฐ๋ผ ๋Œ€ํ™”๋ฅผ ํŽธ์ง‘ํ•˜์„ธ์š”. ํ˜•์‹: 'ํ™”์ž ์ด๋ฆ„: ํ…์ŠคํŠธ'"
1256
  )
1257
 
 
1278
  gr.Examples(
1279
  examples=[
1280
  ["https://huggingface.co/blog/openfree/cycle-navigator", "URL", "Local", "Edge-TTS", "English"],
1281
+ ["", "Keyword", "Local", "Edge-TTS", "English"], # Keyword example
1282
  ["https://huggingface.co/papers/2505.14810", "URL", "Local", "Edge-TTS", "Korean"],
1283
+ ["", "Keyword", "Local", "Edge-TTS", "Korean"], # Korean keyword example
1284
  ],
1285
  inputs=[url_input, input_type_selector, mode_selector, tts_selector, language_selector],
1286
  outputs=[conversation_output, status_output],
 
1288
  cache_examples=False,
1289
  )
1290
 
1291
+ # Input type change handler - ์ˆ˜์ •๋จ
1292
  input_type_selector.change(
1293
  fn=toggle_input_visibility,
1294
  inputs=[input_type_selector],
1295
+ outputs=[url_input, pdf_input, keyword_input]
1296
  )
1297
 
1298
  # ์–ธ์–ด ๋ณ€๊ฒฝ ์‹œ TTS ์—”์ง„ ์˜ต์…˜ ์—…๋ฐ์ดํŠธ
 
1303
  )
1304
 
1305
  # ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ - ์ˆ˜์ •๋œ ๋ถ€๋ถ„
1306
+ def get_article_input(input_type, url_input, pdf_input, keyword_input):
1307
  """Get the appropriate input based on input type"""
1308
  if input_type == "URL":
1309
  return url_input
1310
+ elif input_type == "PDF":
1311
  return pdf_input
1312
+ else: # Keyword
1313
+ return keyword_input
1314
 
1315
  convert_btn.click(
1316
+ fn=lambda input_type, url_input, pdf_input, keyword_input, mode, tts, lang: synthesize_sync(
1317
+ get_article_input(input_type, url_input, pdf_input, keyword_input), input_type, mode, tts, lang
1318
  ),
1319
+ inputs=[input_type_selector, url_input, pdf_input, keyword_input, mode_selector, tts_selector, language_selector],
1320
  outputs=[conversation_output, status_output]
1321
  )
1322