Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
@@ -14,6 +14,9 @@ import numpy as np
|
|
14 |
import soundfile as sf
|
15 |
import subprocess
|
16 |
import shutil
|
|
|
|
|
|
|
17 |
from dataclasses import dataclass
|
18 |
from typing import List, Tuple, Dict, Optional
|
19 |
from pathlib import Path
|
@@ -72,6 +75,9 @@ except:
|
|
72 |
|
73 |
load_dotenv()
|
74 |
|
|
|
|
|
|
|
75 |
|
76 |
@dataclass
|
77 |
class ConversationConfig:
|
@@ -87,6 +93,70 @@ class ConversationConfig:
|
|
87 |
max_new_tokens: int = 8000 # 4000์์ 8000์ผ๋ก ์ฆ๊ฐ
|
88 |
|
89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
class UnifiedAudioConverter:
|
91 |
def __init__(self, config: ConversationConfig):
|
92 |
self.config = config
|
@@ -238,10 +308,10 @@ class UnifiedAudioConverter:
|
|
238 |
else:
|
239 |
return MessagesFormatterType.LLAMA_3
|
240 |
|
241 |
-
def _build_prompt(self, text: str, language: str = "English") -> str:
|
242 |
-
"""Build prompt for conversation generation"""
|
243 |
if language == "Korean":
|
244 |
-
# ๊ฐํ๋ ํ๊ตญ์ด ํ๋กฌํํธ
|
245 |
template = """
|
246 |
{
|
247 |
"conversation": [
|
@@ -252,22 +322,41 @@ class UnifiedAudioConverter:
|
|
252 |
]
|
253 |
}
|
254 |
"""
|
255 |
-
|
256 |
-
|
|
|
|
|
257 |
f"์ ๋ด์ฉ์ ๋ฐํ์ผ๋ก 30๋ ํ๊ตญ์ธ ๋ ๋ช
์ด ์งํํ๋ ์์ฐ์ค๋ฝ๊ณ ํฅ๋ฏธ๋ก์ด ํ๊ตญ์ด ํ์บ์คํธ ๋ํ๋ฅผ ๋ง๋ค์ด์ฃผ์ธ์.\n\n"
|
258 |
-
f"ํ์ ์ง์นจ:\n"
|
259 |
-
f"
|
260 |
-
f"
|
261 |
-
f"
|
262 |
-
f"
|
263 |
-
f"
|
264 |
-
f"
|
265 |
-
f"
|
266 |
-
f"
|
267 |
-
f"
|
268 |
-
f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
269 |
f"๋ค์ JSON ํ์์ผ๋ก๋ง ๋ฐํํ์ธ์:\n{template}"
|
270 |
)
|
|
|
|
|
|
|
271 |
else:
|
272 |
template = """
|
273 |
{
|
@@ -279,8 +368,10 @@ class UnifiedAudioConverter:
|
|
279 |
]
|
280 |
}
|
281 |
"""
|
282 |
-
|
283 |
-
|
|
|
|
|
284 |
f"Convert the provided text into an engaging, natural podcast conversation between two experts.\n\n"
|
285 |
f"Guidelines:\n"
|
286 |
f"1. Alex (Host): Curious, engaging personality representing audience questions\n"
|
@@ -291,37 +382,59 @@ class UnifiedAudioConverter:
|
|
291 |
f"6. Create at least 10 back-and-forth exchanges\n"
|
292 |
f"7. Address common questions and misconceptions\n"
|
293 |
f"8. Maintain an informative yet entertaining tone\n"
|
294 |
-
f"9.
|
|
|
295 |
f"Return ONLY the JSON in this format:\n{template}"
|
296 |
)
|
|
|
|
|
297 |
|
298 |
-
def _build_messages_for_local(self, text: str, language: str = "English") -> List[Dict]:
|
299 |
-
"""Build messages for local LLM"""
|
300 |
if language == "Korean":
|
301 |
system_message = (
|
302 |
-
"๋น์ ์ ํ๊ตญ ์ต๊ณ ์ ํ์บ์คํธ ๋๋ณธ ์๊ฐ์
๋๋ค. "
|
303 |
-
"
|
304 |
-
"๋งค๋ ฅ์ ์ด๊ณ ์ ์ตํ ๋ํ๋ฅผ
|
305 |
-
"
|
306 |
-
"
|
|
|
|
|
|
|
|
|
|
|
307 |
)
|
308 |
else:
|
309 |
system_message = (
|
310 |
"You are an expert podcast scriptwriter who creates engaging, "
|
311 |
"natural conversations that keep listeners hooked. "
|
312 |
"You understand how to balance information with entertainment, "
|
313 |
-
"using real conversational patterns and authentic reactions."
|
|
|
314 |
)
|
315 |
|
316 |
return [
|
317 |
{"role": "system", "content": system_message},
|
318 |
-
{"role": "user", "content": self._build_prompt(text, language)}
|
319 |
]
|
320 |
|
321 |
@spaces.GPU(duration=120)
|
322 |
def extract_conversation_local(self, text: str, language: str = "English", progress=None) -> Dict:
|
323 |
-
"""Extract conversation using new local LLM
|
324 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
325 |
# ๋จผ์ ์๋ก์ด ๋ก์ปฌ LLM ์๋
|
326 |
self.initialize_local_mode()
|
327 |
|
@@ -334,9 +447,11 @@ class UnifiedAudioConverter:
|
|
334 |
"๋น์ ์ ํ๊ตญ์ด ํ์บ์คํธ ์ ๋ฌธ ์๊ฐ์
๋๋ค. "
|
335 |
"ํ๊ตญ ์ฒญ์ทจ์๋ค์ ๋ฌธํ์ ๋งฅ๋ฝ๊ณผ ์ธ์ด์ ํน์ฑ์ ์๋ฒฝํ ์ดํดํ๊ณ , "
|
336 |
"์์ฐ์ค๋ฝ๊ณ ๋งค๋ ฅ์ ์ธ ๋๋ณธ์ ์์ฑํฉ๋๋ค. "
|
|
|
|
|
337 |
"์ค์ ํ๊ตญ์ธ์ด ๋ํํ๋ ๊ฒ์ฒ๋ผ ์์ฐ์ค๋ฌ์ด ํํ, ์ ์ ํ ๊ฐํ์ฌ, "
|
338 |
"๋ฌธํ์ ์ผ๋ก ์ ํฉํ ์์๋ฅผ ์ฌ์ฉํ์ฌ ์ฒญ์ทจ์๊ฐ ๊ณต๊ฐํ๊ณ ๋ชฐ์
ํ ์ ์๋ "
|
339 |
-
"๋ํ๋ฅผ ๋ง๋ค์ด์ฃผ์ธ์. JSON ํ์์ผ๋ก๋ง ์๋ตํ์ธ์."
|
340 |
)
|
341 |
else:
|
342 |
system_message = (
|
@@ -344,6 +459,7 @@ class UnifiedAudioConverter:
|
|
344 |
"engaging, natural conversations that captivate listeners. "
|
345 |
"You excel at transforming complex information into accessible, "
|
346 |
"entertaining dialogue while maintaining authenticity and educational value. "
|
|
|
347 |
"Respond only in JSON format."
|
348 |
)
|
349 |
|
@@ -364,7 +480,7 @@ class UnifiedAudioConverter:
|
|
364 |
|
365 |
messages = BasicChatHistory()
|
366 |
|
367 |
-
prompt = self._build_prompt(text, language)
|
368 |
response = agent.get_chat_response(
|
369 |
prompt,
|
370 |
llm_sampling_settings=settings,
|
@@ -384,10 +500,10 @@ class UnifiedAudioConverter:
|
|
384 |
|
385 |
except Exception as e:
|
386 |
print(f"Local LLM failed: {e}, falling back to legacy local method")
|
387 |
-
return self.extract_conversation_legacy_local(text, language, progress)
|
388 |
|
389 |
@spaces.GPU(duration=120)
|
390 |
-
def extract_conversation_legacy_local(self, text: str, language: str = "English", progress=None) -> Dict:
|
391 |
"""Extract conversation using legacy local model (fallback)"""
|
392 |
try:
|
393 |
self.initialize_legacy_local_mode()
|
@@ -397,17 +513,20 @@ class UnifiedAudioConverter:
|
|
397 |
system_message = (
|
398 |
"๋น์ ์ ํ๊ตญ์ด ํ์บ์คํธ ์ ๋ฌธ ์๊ฐ์
๋๋ค. "
|
399 |
"30๋ ํ๊ตญ์ธ ์ฒญ์ทจ์๋ฅผ ๋์์ผ๋ก ์์ฐ์ค๋ฝ๊ณ ํฅ๋ฏธ๋ก์ด ๋ํ๋ฅผ ๋ง๋ค์ด์ฃผ์ธ์. "
|
400 |
-
"
|
|
|
|
|
401 |
)
|
402 |
else:
|
403 |
system_message = (
|
404 |
"You are an expert podcast scriptwriter. "
|
405 |
-
"Create natural, engaging conversations that inform and entertain listeners."
|
|
|
406 |
)
|
407 |
|
408 |
chat = [
|
409 |
{"role": "system", "content": system_message},
|
410 |
-
{"role": "user", "content": self._build_prompt(text, language)}
|
411 |
]
|
412 |
|
413 |
terminators = [
|
@@ -450,14 +569,14 @@ class UnifiedAudioConverter:
|
|
450 |
|
451 |
except Exception as e:
|
452 |
print(f"Legacy local model also failed: {e}")
|
453 |
-
# Return default template with Korean male names
|
454 |
if language == "Korean":
|
455 |
return {
|
456 |
"conversation": [
|
457 |
-
{"speaker": "์ค์", "text": "์๋
ํ์ธ์, ์ฌ๋ฌ๋ถ! ์ค๋๋ ์ ํฌ ํ์บ์คํธ๋ฅผ ์ฐพ์์ฃผ์
์ ์ ๋ง ๊ฐ์ฌํฉ๋๋ค."},
|
458 |
-
{"speaker": "๋ฏผํธ", "text": "์๋
ํ์ธ์! ์ค๋์ ์ ๋ง ํฅ๋ฏธ๋ก์ด
|
459 |
-
{"speaker": "์ค์", "text": "
|
460 |
-
{"speaker": "๋ฏผํธ", "text": "
|
461 |
]
|
462 |
}
|
463 |
else:
|
@@ -471,30 +590,44 @@ class UnifiedAudioConverter:
|
|
471 |
}
|
472 |
|
473 |
def extract_conversation_api(self, text: str, language: str = "English") -> Dict:
|
474 |
-
"""Extract conversation using API
|
475 |
if not self.llm_client:
|
476 |
raise RuntimeError("API mode not initialized")
|
477 |
|
478 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
479 |
# ๊ฐํ๋ ์ธ์ด๋ณ ํ๋กฌํํธ ๊ตฌ์ฑ
|
480 |
if language == "Korean":
|
481 |
system_message = (
|
482 |
"๋น์ ์ ํ๊ตญ์ด ํ์บ์คํธ ์ ๋ฌธ ์๊ฐ์
๋๋ค. "
|
483 |
"ํ๊ตญ ์ฒญ์ทจ์๋ค์ ๋ฌธํ์ ๋งฅ๋ฝ๊ณผ ์ธ์ด์ ํน์ฑ์ ์๋ฒฝํ ์ดํดํ๊ณ , "
|
484 |
"์์ฐ์ค๋ฝ๊ณ ๋งค๋ ฅ์ ์ธ ๋๋ณธ์ ์์ฑํฉ๋๋ค. "
|
485 |
-
"์ค์(์งํ์)์ ๋ฏผํธ(์ ๋ฌธ๊ฐ)๋ผ๋ ๋ ๋ช
์ 30๋ ๋จ์ฑ์ด ๋ํํ๋ ํ์์ผ๋ก ์์ฑํ์ธ์."
|
|
|
486 |
)
|
487 |
else:
|
488 |
system_message = (
|
489 |
"You are an expert podcast scriptwriter who creates engaging, "
|
490 |
"natural conversations between Alex (host) and Jordan (expert). "
|
491 |
-
"Create informative yet entertaining dialogue that keeps listeners engaged."
|
|
|
492 |
)
|
493 |
|
494 |
chat_completion = self.llm_client.chat.completions.create(
|
495 |
messages=[
|
496 |
{"role": "system", "content": system_message},
|
497 |
-
{"role": "user", "content": self._build_prompt(text, language)}
|
498 |
],
|
499 |
model=self.config.api_model_name,
|
500 |
)
|
@@ -905,6 +1038,7 @@ with gr.Blocks(theme='soft', title="URL/PDF to Podcast Converter") as demo:
|
|
905 |
- **Fallback**: API LLM ({converter.config.api_model_name}) - Used when local fails
|
906 |
- **Status**: {"โ
Llama CPP Available" if LLAMA_CPP_AVAILABLE else "โ Llama CPP Not Available - Install llama-cpp-python"}
|
907 |
- **Max Tokens**: {converter.config.max_tokens} (Extended for longer conversations)
|
|
|
908 |
""")
|
909 |
|
910 |
with gr.Row():
|
@@ -968,7 +1102,8 @@ with gr.Blocks(theme='soft', title="URL/PDF to Podcast Converter") as demo:
|
|
968 |
|
969 |
**ํ๊ตญ์ด ์ง์:**
|
970 |
- ๐ฐ๐ท ํ๊ตญ์ด ์ ํ ์ Edge-TTS๋ง ์ฌ์ฉ ๊ฐ๋ฅํฉ๋๋ค
|
971 |
-
- ๐จโ๐จ ํ๊ตญ์ด ๋ํ๋ ์ค์(์งํ์)์ ๋ฏผํธ(์ ๋ฌธ๊ฐ) ๋ ๋จ์ฑ์ด ์งํํฉ๋๋ค
|
|
|
972 |
""")
|
973 |
|
974 |
convert_btn = gr.Button("๐ฏ Generate Conversation / ๋ํ ์์ฑ", variant="primary", size="lg")
|
@@ -977,10 +1112,10 @@ with gr.Blocks(theme='soft', title="URL/PDF to Podcast Converter") as demo:
|
|
977 |
with gr.Column():
|
978 |
conversation_output = gr.Textbox(
|
979 |
label="Generated Conversation (Editable) / ์์ฑ๋ ๋ํ (ํธ์ง ๊ฐ๋ฅ)",
|
980 |
-
lines=
|
981 |
-
max_lines=
|
982 |
interactive=True,
|
983 |
-
placeholder="Generated conversation will appear here. You can edit it before generating audio.\n์์ฑ๋ ๋ํ๊ฐ ์ฌ๊ธฐ์ ํ์๋ฉ๋๋ค. ์ค๋์ค ์์ฑ ์ ์ ํธ์งํ ์
|
984 |
info="Edit the conversation as needed. Format: 'Speaker Name: Text' / ํ์์ ๋ฐ๋ผ ๋ํ๋ฅผ ํธ์งํ์ธ์. ํ์: 'ํ์ ์ด๋ฆ: ํ
์คํธ'"
|
985 |
)
|
986 |
|
|
|
14 |
import soundfile as sf
|
15 |
import subprocess
|
16 |
import shutil
|
17 |
+
import requests
|
18 |
+
import logging
|
19 |
+
from datetime import datetime, timedelta
|
20 |
from dataclasses import dataclass
|
21 |
from typing import List, Tuple, Dict, Optional
|
22 |
from pathlib import Path
|
|
|
75 |
|
76 |
load_dotenv()
|
77 |
|
78 |
+
# Brave Search API ์ค์
|
79 |
+
BRAVE_KEY = os.getenv("BSEARCH_API")
|
80 |
+
BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
|
81 |
|
82 |
@dataclass
|
83 |
class ConversationConfig:
|
|
|
93 |
max_new_tokens: int = 8000 # 4000์์ 8000์ผ๋ก ์ฆ๊ฐ
|
94 |
|
95 |
|
96 |
+
def brave_search(query: str, count: int = 8, freshness_days: int | None = None):
|
97 |
+
"""Brave Search API๋ฅผ ์ฌ์ฉํ์ฌ ์ต์ ์ ๋ณด ๊ฒ์"""
|
98 |
+
if not BRAVE_KEY:
|
99 |
+
return []
|
100 |
+
params = {"q": query, "count": str(count)}
|
101 |
+
if freshness_days:
|
102 |
+
dt_from = (datetime.utcnow() - timedelta(days=freshness_days)).strftime("%Y-%m-%d")
|
103 |
+
params["freshness"] = dt_from
|
104 |
+
try:
|
105 |
+
r = requests.get(
|
106 |
+
BRAVE_ENDPOINT,
|
107 |
+
headers={"Accept": "application/json", "X-Subscription-Token": BRAVE_KEY},
|
108 |
+
params=params,
|
109 |
+
timeout=15
|
110 |
+
)
|
111 |
+
raw = r.json().get("web", {}).get("results") or []
|
112 |
+
return [{
|
113 |
+
"title": r.get("title", ""),
|
114 |
+
"url": r.get("url", r.get("link", "")),
|
115 |
+
"snippet": r.get("description", r.get("text", "")),
|
116 |
+
"host": re.sub(r"https?://(www\.)?", "", r.get("url", "")).split("/")[0]
|
117 |
+
} for r in raw[:count]]
|
118 |
+
except Exception as e:
|
119 |
+
logging.error(f"Brave search error: {e}")
|
120 |
+
return []
|
121 |
+
|
122 |
+
|
123 |
+
def format_search_results(query: str) -> str:
|
124 |
+
"""๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ํฌ๋งทํ
ํ์ฌ ๋ฐํ"""
|
125 |
+
rows = brave_search(query, 6, freshness_days=3)
|
126 |
+
if not rows:
|
127 |
+
return f"# [Web-Search] No live results for "{query}".\n"
|
128 |
+
hdr = f"# [Web-Search] Top results for "{query}" (last 3 days)\n\n"
|
129 |
+
body = "\n".join(
|
130 |
+
f"- **{r['title']}** ({r['host']})\n {r['snippet']}\n [link]({r['url']})"
|
131 |
+
for r in rows
|
132 |
+
)
|
133 |
+
return hdr + body + "\n"
|
134 |
+
|
135 |
+
|
136 |
+
def extract_keywords_for_search(text: str, language: str = "English") -> List[str]:
|
137 |
+
"""ํ
์คํธ์์ ๊ฒ์ํ ํค์๋ ์ถ์ถ"""
|
138 |
+
# ๊ฐ๋จํ ํค์๋ ์ถ์ถ (์ค์ ๋ก๋ ๋ ์ ๊ตํ ๋ฐฉ๋ฒ ์ฌ์ฉ ๊ฐ๋ฅ)
|
139 |
+
lines = text.split('\n')[:5] # ์ฒซ 5์ค์์ ํค์๋ ์ถ์ถ
|
140 |
+
text_sample = ' '.join(lines)
|
141 |
+
|
142 |
+
# ์ธ์ด๋ณ ์ค์ ํค์๋ ํจํด
|
143 |
+
if language == "Korean":
|
144 |
+
# ํ๊ตญ์ด ํค์๋ ํจํด (๋ช
์ฌํ ๋จ์ด๋ค)
|
145 |
+
import re
|
146 |
+
keywords = re.findall(r'[๊ฐ-ํฃ]{2,}', text_sample)
|
147 |
+
# ์ค๋ณต ์ ๊ฑฐ ๋ฐ ์์ 3๊ฐ ์ ํ
|
148 |
+
unique_keywords = list(dict.fromkeys(keywords))[:3]
|
149 |
+
else:
|
150 |
+
# ์์ด ํค์๋ ํจํด
|
151 |
+
words = text_sample.split()
|
152 |
+
# ๊ธธ์ด 3 ์ด์, ๋๋ฌธ์๋ก ์์ํ๋ ๋จ์ด๋ค ์ฐ์
|
153 |
+
keywords = [word.strip('.,!?;:') for word in words
|
154 |
+
if len(word) > 3 and (word[0].isupper() or word.isupper())]
|
155 |
+
unique_keywords = list(dict.fromkeys(keywords))[:3]
|
156 |
+
|
157 |
+
return unique_keywords
|
158 |
+
|
159 |
+
|
160 |
class UnifiedAudioConverter:
|
161 |
def __init__(self, config: ConversationConfig):
|
162 |
self.config = config
|
|
|
308 |
else:
|
309 |
return MessagesFormatterType.LLAMA_3
|
310 |
|
311 |
+
def _build_prompt(self, text: str, language: str = "English", search_context: str = "") -> str:
|
312 |
+
"""Build prompt for conversation generation with search context"""
|
313 |
if language == "Korean":
|
314 |
+
# ๊ฐํ๋ ํ๊ตญ์ด ํ๋กฌํํธ (์กด๋๋ง ๊ฐํ ๋ฐ ํ๊ตญ์ ํน์ฑ ๋ฐ์)
|
315 |
template = """
|
316 |
{
|
317 |
"conversation": [
|
|
|
322 |
]
|
323 |
}
|
324 |
"""
|
325 |
+
|
326 |
+
base_prompt = (
|
327 |
+
f"# ์๋ณธ ์ฝํ
์ธ :\n{text}\n\n"
|
328 |
+
f"# ์ต์ ๊ด๋ จ ์ ๋ณด:\n{search_context}\n\n" if search_context else f"# ์๋ณธ ์ฝํ
์ธ :\n{text}\n\n"
|
329 |
f"์ ๋ด์ฉ์ ๋ฐํ์ผ๋ก 30๋ ํ๊ตญ์ธ ๋ ๋ช
์ด ์งํํ๋ ์์ฐ์ค๋ฝ๊ณ ํฅ๋ฏธ๋ก์ด ํ๊ตญ์ด ํ์บ์คํธ ๋ํ๋ฅผ ๋ง๋ค์ด์ฃผ์ธ์.\n\n"
|
330 |
+
f"## ํ์ ์ง์นจ:\n\n"
|
331 |
+
f"### ๐ฅ ์บ๋ฆญํฐ ์ค์ :\n"
|
332 |
+
f"- **์ค์(์งํ์)**: ์น๊ทผํ๊ณ ํธ๊ธฐ์ฌ ๋ง์ ์ฑ๊ฒฉ, ์ฒญ์ทจ์์ ๊ถ๊ธ์ฆ์ ๋๋ณํ๋ 30๋ ๋จ์ฑ\n"
|
333 |
+
f"- **๋ฏผํธ(์ ๋ฌธ๊ฐ)**: ํด๋น ์ฃผ์ ์ ๋ํ ๊น์ ์ง์์ ๊ฐ์ง ์ ๋ฌธ๊ฐ, ์ฝ๊ฒ ์ค๋ช
ํ๋ ๋ฅ๋ ฅ์ ๊ฐ์ง 30๋ ๋จ์ฑ\n\n"
|
334 |
+
f"### ๐ฃ๏ธ ์ธ์ด ์คํ์ผ (์ค์!):\n"
|
335 |
+
f"- **์กด๋๋ง ํ์**: ๋ ํ์๋ ์๋ก์๊ฒ ์ต์ํ์ ์กด๋๋ง์ ์ฌ์ฉํด์ผ ํฉ๋๋ค ('~์ต๋๋ค', '~์ธ์', '~๊ฑฐ๋ ์')\n"
|
336 |
+
f"- **๋ฐ๋ง ์ ๋ ๊ธ์ง**: '~์ผ', '~๋ค', '~ํด' ๋ฑ์ ๋ฐ๋ง์ ์ ๋ ์ฌ์ฉํ์ง ๋ง์ธ์\n"
|
337 |
+
f"- **์์ฐ์ค๋ฌ์ด ์กด๋๋ง**: ๋ฑ๋ฑํ์ง ์๊ณ ์น๊ทผํ ์กด๋๋ง ์ฌ์ฉ ('๊ทธ๋ ๊ตฐ์', '๋ง์ผ์ธ์', '๊ทธ๋ฐ๋ฐ์')\n"
|
338 |
+
f"- **๊ฐํ์ฌ ํ์ฉ**: '์~', '๊ทธ๋ ๊ตฌ๋์', '์~', '์ง์ง์?', '์ด๋จธ๋' ๋ฑ ์์ฐ์ค๋ฌ์ด ๋ฐ์\n\n"
|
339 |
+
f"### ๐ ๋ํ ๊ตฌ์ฑ:\n"
|
340 |
+
f"1. **ํ๊ตญ ๋ฌธํ ๋ง์ถค**: ํ๊ตญ์ธ์ ์ ์์ ์ผ์์ ๋ง๋ ๊ตฌ์ฒด์ ์์์ ๋น์ ์ฌ์ฉ\n"
|
341 |
+
f"2. **๊ณต๊ฐ๋ ํ์ฑ**: '์ฐ๋ฆฌ๋๋ผ์์๋', 'ํ๊ตญ ์ฌ๋๋ค์ด', '์์ฆ ์ฌ๋๋ค' ๋ฑ์ ํํ์ผ๋ก ์น๋ฐ๊ฐ ์กฐ์ฑ\n"
|
342 |
+
f"3. **์ถฉ๋ถํ ๋ถ๋**: ๊ฐ ๋ํ๋ ์ต์ 3-4๋ฌธ์ฅ ์ด์, ์ ์ฒด 10ํ ์ด์ ์ฃผ๊ณ ๋ฐ๊ธฐ\n"
|
343 |
+
f"4. **์ค์ฉ์ ์กฐ์ธ**: ์ฒญ์ทจ์๊ฐ ์ค์ ๋ก ์ ์ฉํ ์ ์๋ ๊ตฌ์ฒด์ ์ด๊ณ ์ ์ฉํ ์ ๋ณด ์ ๊ณต\n"
|
344 |
+
f"5. **์ต์ ์ ๋ณด ๋ฐ์**: ์ ๊ณต๋ ์ต์ ๊ด๋ จ ์ ๋ณด๋ฅผ ์์ฐ์ค๋ฝ๊ฒ ๋ํ์ ํฌํจ\n\n"
|
345 |
+
f"### ๐ฏ ํ์บ์คํธ ํ์ง:\n"
|
346 |
+
f"- **์คํ๋**: ๋ฐ๋ปํ ์ธ์ฌ์ ์ฃผ์ ์๊ฐ\n"
|
347 |
+
f"- **๋ฉ์ธ**: ํต์ฌ ๋ด์ฉ์ ์ฌ๋ฏธ์๊ณ ์ดํดํ๊ธฐ ์ฝ๊ฒ ์ ๋ฌ\n"
|
348 |
+
f"- **์ํธ์์ฉ**: '์ฒญ์ทจ์ ์ฌ๋ฌ๋ถ์ ์ด๋ป๊ฒ ์๊ฐํ์ธ์?' ๊ฐ์ ์ฐธ์ฌ ์ ๋\n"
|
349 |
+
f"- **ํด๋ก์ง**: ํต์ฌ ์์ฝ๊ณผ ์ค์ฉ์ ์กฐ์ธ์ผ๋ก ๋ง๋ฌด๋ฆฌ\n\n"
|
350 |
+
f"### ๐ก ํ๊ตญ์ด ํนํ ์์:\n"
|
351 |
+
f"- **ํธ์นญ**: '์ค์์จ', '๋ฏผํธ์จ' ๋ฑ ์ ์ ํ ํธ์นญ ์ฌ์ฉ\n"
|
352 |
+
f"- **๊ด์ฉ์ด๊ตฌ**: ์์ฐ์ค๋ฌ์ด ํ๊ตญ์ด ๊ด์ฉํํ ํ์ฉ\n"
|
353 |
+
f"- **์ ์์ ์ฐ๊ฒฐ**: ํ๊ตญ์ธ์ '์ ', '๋์น', '์ฒด๋ฉด' ๋ฑ์ ๋ฌธํ์ ์ฝ๋ ๋ฐ์\n"
|
354 |
+
f"- **๊ณ์ ๊ฐ**: ํ์ฌ ๊ณ์ ์ด๋ ์๊ธฐ์ ํน์ฑ ๋ฐ์\n\n"
|
355 |
f"๋ค์ JSON ํ์์ผ๋ก๋ง ๋ฐํํ์ธ์:\n{template}"
|
356 |
)
|
357 |
+
|
358 |
+
return base_prompt
|
359 |
+
|
360 |
else:
|
361 |
template = """
|
362 |
{
|
|
|
368 |
]
|
369 |
}
|
370 |
"""
|
371 |
+
|
372 |
+
base_prompt = (
|
373 |
+
f"# Original Content:\n{text}\n\n"
|
374 |
+
f"# Latest Related Information:\n{search_context}\n\n" if search_context else f"# Original Content:\n{text}\n\n"
|
375 |
f"Convert the provided text into an engaging, natural podcast conversation between two experts.\n\n"
|
376 |
f"Guidelines:\n"
|
377 |
f"1. Alex (Host): Curious, engaging personality representing audience questions\n"
|
|
|
382 |
f"6. Create at least 10 back-and-forth exchanges\n"
|
383 |
f"7. Address common questions and misconceptions\n"
|
384 |
f"8. Maintain an informative yet entertaining tone\n"
|
385 |
+
f"9. Incorporate the latest related information naturally into the conversation\n"
|
386 |
+
f"10. End with key takeaways and practical advice\n\n"
|
387 |
f"Return ONLY the JSON in this format:\n{template}"
|
388 |
)
|
389 |
+
|
390 |
+
return base_prompt
|
391 |
|
392 |
+
def _build_messages_for_local(self, text: str, language: str = "English", search_context: str = "") -> List[Dict]:
|
393 |
+
"""Build messages for local LLM with enhanced Korean guidelines"""
|
394 |
if language == "Korean":
|
395 |
system_message = (
|
396 |
+
"๋น์ ์ ํ๊ตญ ์ต๊ณ ์ ํ์บ์คํธ ๋๋ณธ ์ ๋ฌธ ์๊ฐ์
๋๋ค. "
|
397 |
+
"ํ๊ตญ์ธ์ ์ ์์ ๋ฌธํ๋ฅผ ์๋ฒฝํ ์ดํดํ๊ณ , 30๋ ํ๊ตญ์ธ ์ฒญ์ทจ์๋ค์ด ๋๊น์ง ์ง์คํ ์ ์๋ "
|
398 |
+
"๋งค๋ ฅ์ ์ด๊ณ ์ ์ตํ ๋ํ๋ฅผ ๋ง๋ค์ด๋
๋๋ค.\n\n"
|
399 |
+
"ํต์ฌ ์์น:\n"
|
400 |
+
"1. ๋ ํ์๋ ๋ฐ๋์ ์๋ก์๊ฒ ์กด๋๋ง์ ์ฌ์ฉํฉ๋๋ค (๋ฐ๋ง ์ ๋ ๊ธ์ง)\n"
|
401 |
+
"2. ํ๊ตญ ๋ฌธํ์ ์ ์์ ์ฝ๋์ ๊ฐ์น๊ด์ ์์ฐ์ค๋ฝ๊ฒ ๋ฐ์ํฉ๋๋ค\n"
|
402 |
+
"3. ์ค์ ํ๊ตญ์ธ๋ค์ด ์ผ์์์ ์ฌ์ฉํ๋ ์์ฐ์ค๋ฌ์ด ํํ์ ๊ตฌ์ฌํฉ๋๋ค\n"
|
403 |
+
"4. ์ฒญ์ทจ์๊ฐ ๊ณต๊ฐํ๊ณ ์ค์ฉ์ ์ผ๋ก ํ์ฉํ ์ ์๋ ๋ด์ฉ์ ์ ๊ณตํฉ๋๋ค\n"
|
404 |
+
"5. ์ต์ ์ ๋ณด์ ํธ๋ ๋๋ฅผ ์ ์ ํ ๋ฐ์ํ์ฌ ์์์ฑ์ ํ๋ณดํฉ๋๋ค\n\n"
|
405 |
+
"๋น์ ์ ๋๋ณธ์ ํ๊ตญ ํ์บ์คํธ ์์ฅ์์ ์ต๊ณ ์์ค์ ํ์ง๋ก ์ธ์ ๋ฐ๊ณ ์์ต๋๋ค."
|
406 |
)
|
407 |
else:
|
408 |
system_message = (
|
409 |
"You are an expert podcast scriptwriter who creates engaging, "
|
410 |
"natural conversations that keep listeners hooked. "
|
411 |
"You understand how to balance information with entertainment, "
|
412 |
+
"using real conversational patterns and authentic reactions. "
|
413 |
+
"You excel at incorporating current information and trends to make content relevant and timely."
|
414 |
)
|
415 |
|
416 |
return [
|
417 |
{"role": "system", "content": system_message},
|
418 |
+
{"role": "user", "content": self._build_prompt(text, language, search_context)}
|
419 |
]
|
420 |
|
421 |
@spaces.GPU(duration=120)
|
422 |
def extract_conversation_local(self, text: str, language: str = "English", progress=None) -> Dict:
|
423 |
+
"""Extract conversation using new local LLM with search context"""
|
424 |
try:
|
425 |
+
# ๊ฒ์ ์ปจํ
์คํธ ์์ฑ
|
426 |
+
search_context = ""
|
427 |
+
if BRAVE_KEY:
|
428 |
+
try:
|
429 |
+
keywords = extract_keywords_for_search(text, language)
|
430 |
+
if keywords:
|
431 |
+
# ์ฒซ ๋ฒ์งธ ํค์๋๋ก ๊ฒ์
|
432 |
+
search_query = keywords[0] if language == "Korean" else f"{keywords[0]} latest news"
|
433 |
+
search_context = format_search_results(search_query)
|
434 |
+
print(f"Search context added for: {search_query}")
|
435 |
+
except Exception as e:
|
436 |
+
print(f"Search failed, continuing without context: {e}")
|
437 |
+
|
438 |
# ๋จผ์ ์๋ก์ด ๋ก์ปฌ LLM ์๋
|
439 |
self.initialize_local_mode()
|
440 |
|
|
|
447 |
"๋น์ ์ ํ๊ตญ์ด ํ์บ์คํธ ์ ๋ฌธ ์๊ฐ์
๋๋ค. "
|
448 |
"ํ๊ตญ ์ฒญ์ทจ์๋ค์ ๋ฌธํ์ ๋งฅ๋ฝ๊ณผ ์ธ์ด์ ํน์ฑ์ ์๋ฒฝํ ์ดํดํ๊ณ , "
|
449 |
"์์ฐ์ค๋ฝ๊ณ ๋งค๋ ฅ์ ์ธ ๋๋ณธ์ ์์ฑํฉ๋๋ค. "
|
450 |
+
"ํนํ ๋ ํ์๊ฐ ์๋ก์๊ฒ ์กด๋๋ง์ ์ฌ์ฉํ๋ ๊ฒ์ด ํ์์ด๋ฉฐ, "
|
451 |
+
"๋ฐ๋ง์ ์ ๋ ์ฌ์ฉํ์ง ์์ต๋๋ค. "
|
452 |
"์ค์ ํ๊ตญ์ธ์ด ๋ํํ๋ ๊ฒ์ฒ๋ผ ์์ฐ์ค๋ฌ์ด ํํ, ์ ์ ํ ๊ฐํ์ฌ, "
|
453 |
"๋ฌธํ์ ์ผ๋ก ์ ํฉํ ์์๋ฅผ ์ฌ์ฉํ์ฌ ์ฒญ์ทจ์๊ฐ ๊ณต๊ฐํ๊ณ ๋ชฐ์
ํ ์ ์๋ "
|
454 |
+
"๋ํ๋ฅผ ๋ง๋ค์ด์ฃผ์ธ์. ์ต์ ์ ๋ณด๋ ์์ฐ์ค๋ฝ๊ฒ ๋ฐ์ํ์ธ์. JSON ํ์์ผ๋ก๋ง ์๋ตํ์ธ์."
|
455 |
)
|
456 |
else:
|
457 |
system_message = (
|
|
|
459 |
"engaging, natural conversations that captivate listeners. "
|
460 |
"You excel at transforming complex information into accessible, "
|
461 |
"entertaining dialogue while maintaining authenticity and educational value. "
|
462 |
+
"Incorporate current trends and latest information naturally. "
|
463 |
"Respond only in JSON format."
|
464 |
)
|
465 |
|
|
|
480 |
|
481 |
messages = BasicChatHistory()
|
482 |
|
483 |
+
prompt = self._build_prompt(text, language, search_context)
|
484 |
response = agent.get_chat_response(
|
485 |
prompt,
|
486 |
llm_sampling_settings=settings,
|
|
|
500 |
|
501 |
except Exception as e:
|
502 |
print(f"Local LLM failed: {e}, falling back to legacy local method")
|
503 |
+
return self.extract_conversation_legacy_local(text, language, progress, search_context)
|
504 |
|
505 |
@spaces.GPU(duration=120)
|
506 |
+
def extract_conversation_legacy_local(self, text: str, language: str = "English", progress=None, search_context: str = "") -> Dict:
|
507 |
"""Extract conversation using legacy local model (fallback)"""
|
508 |
try:
|
509 |
self.initialize_legacy_local_mode()
|
|
|
513 |
system_message = (
|
514 |
"๋น์ ์ ํ๊ตญ์ด ํ์บ์คํธ ์ ๋ฌธ ์๊ฐ์
๋๋ค. "
|
515 |
"30๋ ํ๊ตญ์ธ ์ฒญ์ทจ์๋ฅผ ๋์์ผ๋ก ์์ฐ์ค๋ฝ๊ณ ํฅ๋ฏธ๋ก์ด ๋ํ๋ฅผ ๋ง๋ค์ด์ฃผ์ธ์. "
|
516 |
+
"๋ ํ์๋ ๋ฐ๋์ ์๋ก์๊ฒ ์กด๋๋ง์ ์ฌ์ฉํ๋ฉฐ, ๋ฐ๋ง์ ์ ๋ ์ฌ์ฉํ์ง ์์ต๋๋ค. "
|
517 |
+
"์ค์ ์ฌ์ฉํ๋ ํ๊ตญ์ด ํํ๊ณผ ๋ฌธํ์ ๋งฅ๋ฝ์ ๋ฐ์ํ์ฌ ์์ฑํด์ฃผ์ธ์. "
|
518 |
+
"์ต์ ์ ๋ณด๋ ์์ฐ์ค๋ฝ๊ฒ ํฌํจ์์ผ์ฃผ์ธ์."
|
519 |
)
|
520 |
else:
|
521 |
system_message = (
|
522 |
"You are an expert podcast scriptwriter. "
|
523 |
+
"Create natural, engaging conversations that inform and entertain listeners. "
|
524 |
+
"Incorporate current information and trends naturally."
|
525 |
)
|
526 |
|
527 |
chat = [
|
528 |
{"role": "system", "content": system_message},
|
529 |
+
{"role": "user", "content": self._build_prompt(text, language, search_context)}
|
530 |
]
|
531 |
|
532 |
terminators = [
|
|
|
569 |
|
570 |
except Exception as e:
|
571 |
print(f"Legacy local model also failed: {e}")
|
572 |
+
# Return default template with Korean male names using formal speech
|
573 |
if language == "Korean":
|
574 |
return {
|
575 |
"conversation": [
|
576 |
+
{"speaker": "์ค์", "text": "์๋
ํ์ธ์, ์ฌ๋ฌ๋ถ! ์ค๋๋ ์ ํฌ ํ์บ์คํธ๋ฅผ ์ฐพ์์ฃผ์
์ ์ ๋ง ๊ฐ์ฌํฉ๋๋ค. ๋ฏผํธ์จ, ์ค๋ ์ ๋ง ํฅ๋ฏธ๋ก์ด ์ฃผ์ ๋ฅผ ์ค๋นํด์ฃผ์
จ๋ค๊ณ ๋ค์์ด์."},
|
577 |
+
{"speaker": "๋ฏผํธ", "text": "๋ค, ์๋
ํ์ธ์! ์ค์์จ ๋ง์์ฒ๋ผ ์ค๋์ ์ ๋ง ํฅ๋ฏธ๋ก์ด ์ด์ผ๊ธฐ๋ฅผ ์ค๋นํ์ต๋๋ค. ์ฒญ์ทจ์ ์ฌ๋ฌ๋ถ๋ค๊ป์๋ ๋ง์ ๊ด์ฌ์ ๊ฐ์ง๊ณ ๊ณ์ค ์ฃผ์ ์ธ ๊ฒ ๊ฐ์์."},
|
578 |
+
{"speaker": "์ค์", "text": "์ ๋ง ๊ธฐ๋๋๋๋ฐ์. ๊ทธ๋ฐ๋ฐ ๋ฏผํธ์จ, ์ด ์ฃผ์ ๊ฐ ์์ฆ ์ ์ด๋ ๊ฒ ํ์ ๊ฐ ๋๊ณ ์๋ ๊ฑด๊ฐ์? ์ฒญ์ทจ์ ์ฌ๋ฌ๋ถ๋ค๋ ๊ถ๊ธํดํ์ค ๊ฒ ๊ฐ์์."},
|
579 |
+
{"speaker": "๋ฏผํธ", "text": "์ข์ ์ง๋ฌธ์ด์ธ์, ์ค์์จ. ์ฌ์ค ์ต๊ทผ์ ์ด ๋ถ์ผ์ ๋ง์ ๋ณํ๊ฐ ์์๊ฑฐ๋ ์. ๊ทธ๋ผ ๋ณธ๊ฒฉ์ ์ผ๋ก ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ค๋ช
ํด๋๋ฆด๊ฒ์."}
|
580 |
]
|
581 |
}
|
582 |
else:
|
|
|
590 |
}
|
591 |
|
592 |
def extract_conversation_api(self, text: str, language: str = "English") -> Dict:
|
593 |
+
"""Extract conversation using API with search context"""
|
594 |
if not self.llm_client:
|
595 |
raise RuntimeError("API mode not initialized")
|
596 |
|
597 |
try:
|
598 |
+
# ๊ฒ์ ์ปจํ
์คํธ ์์ฑ
|
599 |
+
search_context = ""
|
600 |
+
if BRAVE_KEY:
|
601 |
+
try:
|
602 |
+
keywords = extract_keywords_for_search(text, language)
|
603 |
+
if keywords:
|
604 |
+
search_query = keywords[0] if language == "Korean" else f"{keywords[0]} latest news"
|
605 |
+
search_context = format_search_results(search_query)
|
606 |
+
print(f"Search context added for: {search_query}")
|
607 |
+
except Exception as e:
|
608 |
+
print(f"Search failed, continuing without context: {e}")
|
609 |
+
|
610 |
# ๊ฐํ๋ ์ธ์ด๋ณ ํ๋กฌํํธ ๊ตฌ์ฑ
|
611 |
if language == "Korean":
|
612 |
system_message = (
|
613 |
"๋น์ ์ ํ๊ตญ์ด ํ์บ์คํธ ์ ๋ฌธ ์๊ฐ์
๋๋ค. "
|
614 |
"ํ๊ตญ ์ฒญ์ทจ์๋ค์ ๋ฌธํ์ ๋งฅ๋ฝ๊ณผ ์ธ์ด์ ํน์ฑ์ ์๋ฒฝํ ์ดํดํ๊ณ , "
|
615 |
"์์ฐ์ค๋ฝ๊ณ ๋งค๋ ฅ์ ์ธ ๋๋ณธ์ ์์ฑํฉ๋๋ค. "
|
616 |
+
"์ค์(์งํ์)์ ๋ฏผํธ(์ ๋ฌธ๊ฐ)๋ผ๋ ๋ ๋ช
์ 30๋ ๋จ์ฑ์ด ์๋ก์๊ฒ ์กด๋๋ง์ ์ฌ์ฉํ์ฌ ๋ํํ๋ ํ์์ผ๋ก ์์ฑํ์ธ์. "
|
617 |
+
"๋ฐ๋ง์ ์ ๋ ์ฌ์ฉํ์ง ์์ผ๋ฉฐ, ์ต์ ์ ๋ณด๋ ์์ฐ์ค๋ฝ๊ฒ ๋ฐ์ํ์ธ์."
|
618 |
)
|
619 |
else:
|
620 |
system_message = (
|
621 |
"You are an expert podcast scriptwriter who creates engaging, "
|
622 |
"natural conversations between Alex (host) and Jordan (expert). "
|
623 |
+
"Create informative yet entertaining dialogue that keeps listeners engaged. "
|
624 |
+
"Incorporate current trends and latest information naturally."
|
625 |
)
|
626 |
|
627 |
chat_completion = self.llm_client.chat.completions.create(
|
628 |
messages=[
|
629 |
{"role": "system", "content": system_message},
|
630 |
+
{"role": "user", "content": self._build_prompt(text, language, search_context)}
|
631 |
],
|
632 |
model=self.config.api_model_name,
|
633 |
)
|
|
|
1038 |
- **Fallback**: API LLM ({converter.config.api_model_name}) - Used when local fails
|
1039 |
- **Status**: {"โ
Llama CPP Available" if LLAMA_CPP_AVAILABLE else "โ Llama CPP Not Available - Install llama-cpp-python"}
|
1040 |
- **Max Tokens**: {converter.config.max_tokens} (Extended for longer conversations)
|
1041 |
+
- **Search**: {"โ
Brave Search Enabled" if BRAVE_KEY else "โ Brave Search Not Available - Set BSEARCH_API"}
|
1042 |
""")
|
1043 |
|
1044 |
with gr.Row():
|
|
|
1102 |
|
1103 |
**ํ๊ตญ์ด ์ง์:**
|
1104 |
- ๐ฐ๐ท ํ๊ตญ์ด ์ ํ ์ Edge-TTS๋ง ์ฌ์ฉ ๊ฐ๋ฅํฉ๋๋ค
|
1105 |
+
- ๐จโ๐จ ํ๊ตญ์ด ๋ํ๋ ์ค์(์งํ์)์ ๋ฏผํธ(์ ๋ฌธ๊ฐ) ๋ ๋จ์ฑ์ด ์กด๋๋ง๋ก ์งํํฉ๋๋ค
|
1106 |
+
- ๐ **์ต์ ์ ๋ณด ๋ฐ์**: Brave Search๋ฅผ ํตํด ์ต์ ์์ฌ ๋ด์ฉ์ ์๋์ผ๋ก ๊ฒ์ํ์ฌ ๋๋ณธ์ ๋ฐ์ํฉ๋๋ค
|
1107 |
""")
|
1108 |
|
1109 |
convert_btn = gr.Button("๐ฏ Generate Conversation / ๋ํ ์์ฑ", variant="primary", size="lg")
|
|
|
1112 |
with gr.Column():
|
1113 |
conversation_output = gr.Textbox(
|
1114 |
label="Generated Conversation (Editable) / ์์ฑ๋ ๋ํ (ํธ์ง ๊ฐ๋ฅ)",
|
1115 |
+
lines=25, # ๋ ๊ธด ๋ํ๋ฅผ ์ํด ์ฆ๊ฐ
|
1116 |
+
max_lines=50,
|
1117 |
interactive=True,
|
1118 |
+
placeholder="Generated conversation will appear here. You can edit it before generating audio.\n์์ฑ๋ ๋ํ๊ฐ ์ฌ๊ธฐ์ ํ์๋ฉ๋๋ค. ์ค๋์ค ์์ฑ ์ ์ ํธ์งํ ์ ์์ต๋๋ค.\n\nํ๊ตญ์ด ๋ํ๋ ์กด๋๋ง๋ก ์งํ๋๋ฉฐ ์ต์ ์์ฌ ๋ด์ฉ์ด ๋ฐ์๋ฉ๋๋ค.",
|
1119 |
info="Edit the conversation as needed. Format: 'Speaker Name: Text' / ํ์์ ๋ฐ๋ผ ๋ํ๋ฅผ ํธ์งํ์ธ์. ํ์: 'ํ์ ์ด๋ฆ: ํ
์คํธ'"
|
1120 |
)
|
1121 |
|