Added live text, show extraction, add website
Browse files
app.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
| 1 |
# Copyright: Shayekh Bin Islam. KAIST, South Korea. 2026.
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
try:
|
| 5 |
import spaces
|
|
@@ -54,6 +58,53 @@ def extract_pdf_content(pdf_path, max_pages=2):
|
|
| 54 |
images.append(img)
|
| 55 |
return text, images
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
def get_base64_image(image):
|
| 59 |
buffered = io.BytesIO()
|
|
@@ -62,13 +113,13 @@ def get_base64_image(image):
|
|
| 62 |
return f"data:image/jpeg;base64,{img_str}"
|
| 63 |
|
| 64 |
@spaces.GPU(duration=120)
|
| 65 |
-
def extract_vocabulary(pdf_text, images, translit_lang, translit_format, target_lang):
|
| 66 |
"""Use Transformers to extract vocabulary from text and images."""
|
| 67 |
global model, processor
|
| 68 |
|
| 69 |
os.makedirs("log", exist_ok=True)
|
| 70 |
|
| 71 |
-
if pdf_text.strip() ==
|
| 72 |
pdf_text = '''"No Text available, see provided images only."'''
|
| 73 |
|
| 74 |
|
|
@@ -76,16 +127,20 @@ def extract_vocabulary(pdf_text, images, translit_lang, translit_format, target_
|
|
| 76 |
if translit_lang.upper() != "ENGLISH":
|
| 77 |
non_english = f" CRITICAL: You MUST use the native alphabet/script of {translit_lang.upper()}, do NOT use English letters unless requested."
|
| 78 |
|
| 79 |
-
prompt_text = f"""Extract
|
|
|
|
|
|
|
|
|
|
| 80 |
Return ONLY a valid JSON list of dictionaries, where each dictionary has four keys:
|
| 81 |
- 'korean' (the Korean text)
|
| 82 |
- 'transliteration' (the pronunciation transliterated into {translit_lang.upper()} script/characters, formatted as {translit_format}.{non_english})
|
| 83 |
- 'translation' (the translation into {target_lang.upper()})
|
| 84 |
- 'explanation' (a brief grammar or context note in {target_lang.upper()}).
|
| 85 |
No markdown formatting, just raw JSON with ```json and ``` markers.
|
|
|
|
| 86 |
|
| 87 |
Text:
|
| 88 |
-
{pdf_text[:
|
| 89 |
"""
|
| 90 |
|
| 91 |
# DEBUG: Log prompt text
|
|
@@ -122,13 +177,112 @@ Text:
|
|
| 122 |
return_tensors="pt",
|
| 123 |
padding=True
|
| 124 |
).to("cuda")
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
|
|
|
|
| 127 |
**inputs,
|
| 128 |
-
|
| 129 |
max_new_tokens=2048*16,
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
do_sample=True
|
| 133 |
)
|
| 134 |
|
|
@@ -137,24 +291,22 @@ Text:
|
|
| 137 |
]
|
| 138 |
output_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
| 139 |
|
| 140 |
-
# DEBUG: Log raw output text
|
| 141 |
-
with open("log/
|
| 142 |
f.write(output_text)
|
| 143 |
|
| 144 |
except Exception as e:
|
| 145 |
-
print(f"Error during Transformers inference: {e}")
|
| 146 |
return []
|
| 147 |
|
| 148 |
try:
|
| 149 |
import re
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
clean_text = json_match.group(1).strip()
|
| 154 |
else:
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
clean_text = json_match.group(1).strip() if json_match else output_text.strip()
|
| 158 |
|
| 159 |
data = json.loads(clean_text)
|
| 160 |
if not isinstance(data, list):
|
|
@@ -172,8 +324,14 @@ def numpy_to_base64_audio(wav, sample_rate):
|
|
| 172 |
audio_base64 = base64.b64encode(buffer.read()).decode('utf-8')
|
| 173 |
return f"data:audio/wav;base64,{audio_base64}"
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
@spaces.GPU(duration=120)
|
| 176 |
-
def process_pdf(pdf_file, translit_lang, translit_format, target_lang, progress=gr.Progress()):
|
| 177 |
global tts, voice_style
|
| 178 |
|
| 179 |
# Clean language choices from "Family - Language" to just "Language"
|
|
@@ -184,26 +342,59 @@ def process_pdf(pdf_file, translit_lang, translit_format, target_lang, progress=
|
|
| 184 |
|
| 185 |
os.makedirs("log", exist_ok=True)
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
try:
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
except Exception as e:
|
| 196 |
-
|
|
|
|
| 197 |
|
| 198 |
vocab_list = []
|
|
|
|
| 199 |
for attempt in range(1, 4):
|
| 200 |
progress(0.2, desc=f"Extracting vocabulary (Attempt {attempt}/3)...")
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
if vocab_list:
|
| 203 |
break
|
| 204 |
|
| 205 |
if not vocab_list:
|
| 206 |
-
|
|
|
|
| 207 |
|
| 208 |
progress(0.6, desc="Generating TTS audio...")
|
| 209 |
# Pre-generate TTS audio
|
|
@@ -214,7 +405,11 @@ def process_pdf(pdf_file, translit_lang, translit_format, target_lang, progress=
|
|
| 214 |
korean += "."
|
| 215 |
|
| 216 |
try:
|
| 217 |
-
wav, dur = tts.synthesize(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
# DEBUG: Save audio locally
|
| 220 |
wav_1d = wav.squeeze()
|
|
@@ -250,8 +445,8 @@ def process_pdf(pdf_file, translit_lang, translit_format, target_lang, progress=
|
|
| 250 |
}}
|
| 251 |
.flashcard {{
|
| 252 |
width: 100%;
|
| 253 |
-
height:
|
| 254 |
-
|
| 255 |
transition: transform 0.6s cubic-bezier(0.4, 0.2, 0.2, 1);
|
| 256 |
transform-style: preserve-3d;
|
| 257 |
cursor: pointer;
|
|
@@ -260,9 +455,8 @@ def process_pdf(pdf_file, translit_lang, translit_format, target_lang, progress=
|
|
| 260 |
transform: rotateY(180deg);
|
| 261 |
}}
|
| 262 |
.card-face {{
|
| 263 |
-
|
| 264 |
width: 100%;
|
| 265 |
-
height: 100%;
|
| 266 |
backface-visibility: hidden;
|
| 267 |
display: flex;
|
| 268 |
flex-direction: column;
|
|
@@ -370,6 +564,16 @@ def process_pdf(pdf_file, translit_lang, translit_format, target_lang, progress=
|
|
| 370 |
font-size: 14px;
|
| 371 |
font-weight: 600;
|
| 372 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
</style>
|
| 374 |
</head>
|
| 375 |
<body>
|
|
@@ -377,12 +581,18 @@ def process_pdf(pdf_file, translit_lang, translit_format, target_lang, progress=
|
|
| 377 |
<div class="flashcard-container">
|
| 378 |
<div class="flashcard" id="card" onclick="flipCard()">
|
| 379 |
<div class="card-face card-front">
|
| 380 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 381 |
<button class="audio-btn" onclick="playAudio(event)" id="audio-btn" style="display:none;"><i class="fi fi-rr-play-circle"></i> Play Audio</button>
|
| 382 |
<p style="margin-top:20px; color:#999; font-size:13px; display:flex; align-items:center; gap:5px;"><i class="fi fi-rr-rotate-right"></i> Click card to flip 🎯</p>
|
| 383 |
</div>
|
| 384 |
<div class="card-face card-back">
|
| 385 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 386 |
<div class="translit-text" id="back-translit"></div>
|
| 387 |
<div class="explanation-text"><i class="fi fi-rr-lightbulb-on" style="color:#f1c40f;"></i> <span id="back-exp"></span></div>
|
| 388 |
</div>
|
|
@@ -405,6 +615,8 @@ def process_pdf(pdf_file, translit_lang, translit_format, target_lang, progress=
|
|
| 405 |
document.getElementById('front-text').innerHTML = "No vocabulary found 😥";
|
| 406 |
document.getElementById('prev-btn').disabled = true;
|
| 407 |
document.getElementById('next-btn').disabled = true;
|
|
|
|
|
|
|
| 408 |
return;
|
| 409 |
}}
|
| 410 |
const card = cards[currentIndex];
|
|
@@ -413,6 +625,9 @@ def process_pdf(pdf_file, translit_lang, translit_format, target_lang, progress=
|
|
| 413 |
document.getElementById('back-translit').innerText = card.transliteration ? `[${{card.transliteration}}]` : "";
|
| 414 |
document.getElementById('back-exp').innerText = card.explanation || "";
|
| 415 |
|
|
|
|
|
|
|
|
|
|
| 416 |
document.getElementById('prev-btn').disabled = currentIndex === 0;
|
| 417 |
document.getElementById('next-btn').disabled = currentIndex === cards.length - 1;
|
| 418 |
document.getElementById('progress-text').innerHTML = `📚 Card ${{currentIndex + 1}} of ${{cards.length}}`;
|
|
@@ -452,6 +667,20 @@ def process_pdf(pdf_file, translit_lang, translit_format, target_lang, progress=
|
|
| 452 |
}}
|
| 453 |
}}
|
| 454 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
window.onload = function() {{
|
| 456 |
updateCard();
|
| 457 |
}};
|
|
@@ -464,7 +693,7 @@ def process_pdf(pdf_file, translit_lang, translit_format, target_lang, progress=
|
|
| 464 |
safe_srcdoc = html.escape(iframe_html)
|
| 465 |
|
| 466 |
# Return the iframe containing the whole SPA
|
| 467 |
-
|
| 468 |
|
| 469 |
LANGUAGE_DATA = """Indo-European English, French, Portuguese, German, Romanian, Swedish, Danish, Bulgarian, Russian, Czech, Greek, Ukrainian, Spanish, Dutch, Slovak, Croatian, Polish, Lithuanian, Norwegian Bokmål, Norwegian Nynorsk, Persian, Slovenian, Gujarati, Latvian, Italian, Occitan, Nepali, Marathi, Belarusian, Serbian, Luxembourgish, Venetian, Assamese, Welsh, Silesian, Asturian, Chhattisgarhi, Awadhi, Maithili, Bhojpuri, Sindhi, Irish, Faroese, Hindi, Punjabi, Bengali, Oriya, Tajik, Eastern Yiddish, Lombard, Ligurian, Sicilian, Friulian, Sardinian, Galician, Catalan, Icelandic, Tosk Albanian, Limburgish, Dari, Afrikaans, Macedonian, Sinhala, Urdu, Magahi, Bosnian, Armenian, Latgalian, Scottish Gaelic, Central Kurdish, Northern Kurdish, Southern Pashto, Sanskrit, Dhundari, Marwari, Ahirani, Bagheli, Bagri, Bundeli, Braj, Kumaoni, Kashmiri
|
| 470 |
Sino-Tibetan Chinese (Simplified), Chinese (Traditional), Cantonese, Burmese, Standard Tibetan, Meitei
|
|
@@ -499,13 +728,22 @@ def get_example_pdf():
|
|
| 499 |
def create_demo():
|
| 500 |
example_pdf = get_example_pdf()
|
| 501 |
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
|
| 506 |
with gr.Row():
|
| 507 |
with gr.Column(scale=1):
|
| 508 |
-
|
|
|
|
| 509 |
|
| 510 |
gr.Markdown("### ⚙️ Customization Settings")
|
| 511 |
translit_lang = gr.Dropdown(
|
|
@@ -519,16 +757,42 @@ def create_demo():
|
|
| 519 |
choices=LANGUAGE_CHOICES,
|
| 520 |
value="Indo-European - English"
|
| 521 |
)
|
|
|
|
| 522 |
|
| 523 |
-
|
|
|
|
|
|
|
| 524 |
|
| 525 |
with gr.Column(scale=2):
|
| 526 |
output_html = gr.HTML(label="Flashcards will appear here")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
|
| 528 |
-
|
|
|
|
|
|
|
|
|
|
| 529 |
fn=process_pdf,
|
| 530 |
-
inputs=[pdf_input, translit_lang, translit_format, target_lang],
|
| 531 |
-
outputs=output_html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
)
|
| 533 |
return demo
|
| 534 |
|
|
@@ -549,6 +813,162 @@ if __name__ == "__main__":
|
|
| 549 |
# processor.chat_template = f.read()
|
| 550 |
# except Exception as e:
|
| 551 |
# print("Could not load custom chat template:", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
|
| 553 |
model = AutoModelForImageTextToText.from_pretrained(
|
| 554 |
model_id,
|
|
|
|
| 1 |
# Copyright: Shayekh Bin Islam. KAIST, South Korea. 2026.
|
| 2 |
|
| 3 |
+
MAX_TEXT_CHAR = 1500
|
| 4 |
+
|
| 5 |
+
# model_id = "Qwen/Qwen3.5-9B"
|
| 6 |
+
model_id = "Qwen/Qwen3.5-2B"
|
| 7 |
|
| 8 |
try:
|
| 9 |
import spaces
|
|
|
|
| 58 |
images.append(img)
|
| 59 |
return text, images
|
| 60 |
|
| 61 |
+
def extract_website_content(url, max_images=2):
|
| 62 |
+
"""Extract text and images from a website URL."""
|
| 63 |
+
import requests
|
| 64 |
+
from bs4 import BeautifulSoup
|
| 65 |
+
import io
|
| 66 |
+
|
| 67 |
+
headers = {
|
| 68 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
| 69 |
+
}
|
| 70 |
+
response = requests.get(url, headers=headers, timeout=10)
|
| 71 |
+
response.raise_for_status()
|
| 72 |
+
|
| 73 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
| 74 |
+
|
| 75 |
+
for script in soup(["script", "style", "nav", "footer", "header", "noscript"]):
|
| 76 |
+
script.extract()
|
| 77 |
+
|
| 78 |
+
text = soup.get_text(separator='\n')
|
| 79 |
+
lines = (line.strip() for line in text.splitlines())
|
| 80 |
+
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
| 81 |
+
text = '\n'.join(chunk for chunk in chunks if chunk)
|
| 82 |
+
|
| 83 |
+
images = []
|
| 84 |
+
img_tags = soup.find_all('img')
|
| 85 |
+
for img in img_tags:
|
| 86 |
+
if len(images) >= max_images:
|
| 87 |
+
break
|
| 88 |
+
src = img.get('src') or img.get('data-src')
|
| 89 |
+
if src:
|
| 90 |
+
if src.startswith('//'):
|
| 91 |
+
src = 'https:' + src
|
| 92 |
+
elif src.startswith('/'):
|
| 93 |
+
from urllib.parse import urljoin
|
| 94 |
+
src = urljoin(url, src)
|
| 95 |
+
|
| 96 |
+
try:
|
| 97 |
+
img_resp = requests.get(src, headers=headers, timeout=5)
|
| 98 |
+
if img_resp.status_code == 200:
|
| 99 |
+
pil_img = Image.open(io.BytesIO(img_resp.content))
|
| 100 |
+
if pil_img.mode != 'RGB':
|
| 101 |
+
pil_img = pil_img.convert('RGB')
|
| 102 |
+
if pil_img.width >= 100 and pil_img.height >= 100:
|
| 103 |
+
images.append(pil_img)
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f"Failed to load image {src}: {e}")
|
| 106 |
+
|
| 107 |
+
return text, images
|
| 108 |
|
| 109 |
def get_base64_image(image):
|
| 110 |
buffered = io.BytesIO()
|
|
|
|
| 113 |
return f"data:image/jpeg;base64,{img_str}"
|
| 114 |
|
| 115 |
@spaces.GPU(duration=120)
|
| 116 |
+
def extract_vocabulary(pdf_text, images, translit_lang, translit_format, target_lang, max_text_char=1500):
|
| 117 |
"""Use Transformers to extract vocabulary from text and images."""
|
| 118 |
global model, processor
|
| 119 |
|
| 120 |
os.makedirs("log", exist_ok=True)
|
| 121 |
|
| 122 |
+
if len(pdf_text.strip()) == 0:
|
| 123 |
pdf_text = '''"No Text available, see provided images only."'''
|
| 124 |
|
| 125 |
|
|
|
|
| 127 |
if translit_lang.upper() != "ENGLISH":
|
| 128 |
non_english = f" CRITICAL: You MUST use the native alphabet/script of {translit_lang.upper()}, do NOT use English letters unless requested."
|
| 129 |
|
| 130 |
+
prompt_text = f"""Extract at least 10 key Korean words or phrases from the following text and images.
|
| 131 |
+
Focus on meaningful vocabulary that is highly helpful for a new language learner (e.g., common nouns, verbs, adjectives, or useful expressions).
|
| 132 |
+
CRITICAL: Do NOT extract website template words, navigation menus, boilerplate text, UI elements, or titles like 'Home page', 'News', 'Menu'.
|
| 133 |
+
|
| 134 |
Return ONLY a valid JSON list of dictionaries, where each dictionary has four keys:
|
| 135 |
- 'korean' (the Korean text)
|
| 136 |
- 'transliteration' (the pronunciation transliterated into {translit_lang.upper()} script/characters, formatted as {translit_format}.{non_english})
|
| 137 |
- 'translation' (the translation into {target_lang.upper()})
|
| 138 |
- 'explanation' (a brief grammar or context note in {target_lang.upper()}).
|
| 139 |
No markdown formatting, just raw JSON with ```json and ``` markers.
|
| 140 |
+
CRITICAL: Do NOT provide any conversational filler, thinking steps, or reasoning. Answer quick without very long thinking. Output the JSON array IMMEDIATELY.
|
| 141 |
|
| 142 |
Text:
|
| 143 |
+
{pdf_text[:int(max_text_char)]}
|
| 144 |
"""
|
| 145 |
|
| 146 |
# DEBUG: Log prompt text
|
|
|
|
| 177 |
return_tensors="pt",
|
| 178 |
padding=True
|
| 179 |
).to("cuda")
|
| 180 |
+
|
| 181 |
+
from transformers import TextIteratorStreamer
|
| 182 |
+
from threading import Thread
|
| 183 |
|
| 184 |
+
streamer = TextIteratorStreamer(processor.tokenizer, skip_prompt=True, skip_special_tokens=True)
|
| 185 |
+
generation_kwargs = dict(
|
| 186 |
**inputs,
|
| 187 |
+
streamer=streamer,
|
| 188 |
max_new_tokens=2048*16,
|
| 189 |
+
do_sample=True,
|
| 190 |
+
repetition_penalty=1.1,
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
if len(images) > 0:
|
| 194 |
+
generation_kwargs.update(dict(temperature=0.6, top_p=0.95, top_k=20, min_p=0.0))
|
| 195 |
+
else:
|
| 196 |
+
generation_kwargs.update(dict(temperature=1.0, top_p=0.95, top_k=20, min_p=0.0))
|
| 197 |
+
|
| 198 |
+
thread = Thread(target=model.generate, kwargs=generation_kwargs)
|
| 199 |
+
thread.start()
|
| 200 |
+
|
| 201 |
+
output_text = ""
|
| 202 |
+
for new_text in streamer:
|
| 203 |
+
output_text += new_text
|
| 204 |
+
yield output_text, None
|
| 205 |
+
|
| 206 |
+
# DEBUG: Log raw output text
|
| 207 |
+
with open("log/debug_vlm_output.txt", "w", encoding="utf-8") as f:
|
| 208 |
+
f.write(output_text)
|
| 209 |
+
|
| 210 |
+
except Exception as e:
|
| 211 |
+
print(f"Error during Transformers inference: {e}")
|
| 212 |
+
yield f"Error during Transformers inference: {e}", []
|
| 213 |
+
return
|
| 214 |
+
|
| 215 |
+
try:
|
| 216 |
+
import re
|
| 217 |
+
# Extract JSON from markdown code fences or raw output
|
| 218 |
+
json_matches = list(re.finditer(r'```(?:json)?\s*([\s\S]*?)```', output_text))
|
| 219 |
+
if json_matches:
|
| 220 |
+
clean_text = json_matches[-1].group(1).strip()
|
| 221 |
+
else:
|
| 222 |
+
# Fallback: find last [ ... ] or { ... } block
|
| 223 |
+
json_matches = list(re.finditer(r'(\[[\s\S]*\]|\{[\s\S]*\})', output_text))
|
| 224 |
+
clean_text = json_matches[-1].group(1).strip() if json_matches else output_text.strip()
|
| 225 |
+
|
| 226 |
+
data = json.loads(clean_text)
|
| 227 |
+
if not isinstance(data, list):
|
| 228 |
+
data = [data]
|
| 229 |
+
yield output_text, data
|
| 230 |
+
except Exception as e:
|
| 231 |
+
print(f"Error parsing JSON: {e}\nRaw output: {output_text}")
|
| 232 |
+
yield output_text, []
|
| 233 |
+
|
| 234 |
+
def translate_vocabulary(korean_words, translit_lang, translit_format, target_lang):
|
| 235 |
+
"""Use Transformers text-only inference to translate/transliterate Korean words."""
|
| 236 |
+
global model, processor
|
| 237 |
+
|
| 238 |
+
non_english = ""
|
| 239 |
+
if translit_lang.upper() != "ENGLISH":
|
| 240 |
+
non_english = f" CRITICAL: You MUST use the native alphabet/script of {translit_lang.upper()}, do NOT use English letters unless requested."
|
| 241 |
+
|
| 242 |
+
words_str = ", ".join(korean_words)
|
| 243 |
+
prompt_text = f"""Translate and transliterate the following Korean words.
|
| 244 |
+
Return ONLY a valid JSON list of dictionaries, where each dictionary has four keys:
|
| 245 |
+
- 'korean' (the original Korean text)
|
| 246 |
+
- 'transliteration' (the pronunciation transliterated into {translit_lang.upper()} script/characters, formatted as {translit_format}.{non_english})
|
| 247 |
+
- 'translation' (the translation into {target_lang.upper()})
|
| 248 |
+
- 'explanation' (a brief grammar or context note in {target_lang.upper()}).
|
| 249 |
+
No markdown formatting, just raw JSON with ```json and ``` markers.
|
| 250 |
+
CRITICAL: Do NOT provide any conversational filler, thinking steps, or reasoning. Answer quick without very long thinking. Output the JSON array IMMEDIATELY.
|
| 251 |
+
|
| 252 |
+
Korean words:
|
| 253 |
+
{words_str}
|
| 254 |
+
"""
|
| 255 |
+
|
| 256 |
+
# DEBUG: Log translation prompt text
|
| 257 |
+
with open("log/debug_translate_prompt.txt", "w", encoding="utf-8") as f:
|
| 258 |
+
f.write(prompt_text)
|
| 259 |
+
|
| 260 |
+
messages = [
|
| 261 |
+
{
|
| 262 |
+
"role": "user",
|
| 263 |
+
"content": [{"type": "text", "text": prompt_text}]
|
| 264 |
+
}
|
| 265 |
+
]
|
| 266 |
+
|
| 267 |
+
try:
|
| 268 |
+
model.to("cuda")
|
| 269 |
+
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 270 |
+
inputs = processor(
|
| 271 |
+
text=[text],
|
| 272 |
+
images=None,
|
| 273 |
+
return_tensors="pt",
|
| 274 |
+
padding=True
|
| 275 |
+
).to("cuda")
|
| 276 |
+
|
| 277 |
+
generated_ids = model.generate(
|
| 278 |
+
**inputs,
|
| 279 |
+
# max_new_tokens=2048*16,
|
| 280 |
+
max_new_tokens=2048*2,
|
| 281 |
+
# temperature=1.0,
|
| 282 |
+
# top_p=0.95,
|
| 283 |
+
temperature=1.0, top_p=0.95, top_k=20, min_p=0.0,
|
| 284 |
+
# presence_penalty=1.5,
|
| 285 |
+
repetition_penalty=1.1,
|
| 286 |
do_sample=True
|
| 287 |
)
|
| 288 |
|
|
|
|
| 291 |
]
|
| 292 |
output_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
| 293 |
|
| 294 |
+
# DEBUG: Log raw translation output text
|
| 295 |
+
with open("log/debug_translate_output.txt", "w", encoding="utf-8") as f:
|
| 296 |
f.write(output_text)
|
| 297 |
|
| 298 |
except Exception as e:
|
| 299 |
+
print(f"Error during Transformers text inference: {e}")
|
| 300 |
return []
|
| 301 |
|
| 302 |
try:
|
| 303 |
import re
|
| 304 |
+
json_matches = list(re.finditer(r'```(?:json)?\s*([\s\S]*?)```', output_text))
|
| 305 |
+
if json_matches:
|
| 306 |
+
clean_text = json_matches[-1].group(1).strip()
|
|
|
|
| 307 |
else:
|
| 308 |
+
json_matches = list(re.finditer(r'(\[[\s\S]*\]|\{[\s\S]*\})', output_text))
|
| 309 |
+
clean_text = json_matches[-1].group(1).strip() if json_matches else output_text.strip()
|
|
|
|
| 310 |
|
| 311 |
data = json.loads(clean_text)
|
| 312 |
if not isinstance(data, list):
|
|
|
|
| 324 |
audio_base64 = base64.b64encode(buffer.read()).decode('utf-8')
|
| 325 |
return f"data:audio/wav;base64,{audio_base64}"
|
| 326 |
|
| 327 |
+
import hashlib
|
| 328 |
+
|
| 329 |
+
def hash_file(filepath):
|
| 330 |
+
with open(filepath, 'rb') as f:
|
| 331 |
+
return hashlib.md5(f.read(1024*1024)).hexdigest()
|
| 332 |
+
|
| 333 |
@spaces.GPU(duration=120)
|
| 334 |
+
def process_pdf(pdf_file, url_input, translit_lang, translit_format, target_lang, max_text_char, last_source_hash, last_korean_words, progress=gr.Progress()):
|
| 335 |
global tts, voice_style
|
| 336 |
|
| 337 |
# Clean language choices from "Family - Language" to just "Language"
|
|
|
|
| 342 |
|
| 343 |
os.makedirs("log", exist_ok=True)
|
| 344 |
|
| 345 |
+
is_url = bool(url_input and url_input.strip())
|
| 346 |
+
if pdf_file is None and not is_url:
|
| 347 |
+
yield "<p>Please upload a PDF or enter a URL.</p>", None, None, ""
|
| 348 |
+
return
|
| 349 |
+
|
| 350 |
+
if is_url:
|
| 351 |
+
current_source_hash = hashlib.md5(url_input.strip().encode()).hexdigest()
|
| 352 |
+
else:
|
| 353 |
+
current_source_hash = hash_file(pdf_file.name)
|
| 354 |
+
|
| 355 |
+
vocab_list = []
|
| 356 |
+
|
| 357 |
+
# if last_source_hash == current_source_hash and last_korean_words:
|
| 358 |
+
# # Just run text-to-text LLM
|
| 359 |
+
# progress(0.2, desc="Translating previously extracted vocabulary...")
|
| 360 |
+
# korean_words = [item.get("korean") for item in last_korean_words if item.get("korean")]
|
| 361 |
+
# for attempt in range(1, 4):
|
| 362 |
+
# vocab_list = translate_vocabulary(korean_words, translit_lang, translit_format, target_lang)
|
| 363 |
+
# if vocab_list:
|
| 364 |
+
# break
|
| 365 |
+
# else:
|
| 366 |
|
| 367 |
try:
|
| 368 |
+
if is_url:
|
| 369 |
+
progress(0, desc="Fetching Website...")
|
| 370 |
+
content_text, images = extract_website_content(url_input.strip())
|
| 371 |
+
else:
|
| 372 |
+
progress(0, desc="Reading PDF...")
|
| 373 |
+
content_text, images = extract_pdf_content(pdf_file.name)
|
| 374 |
+
|
| 375 |
+
if not content_text.strip() and not images:
|
| 376 |
+
yield "<p>No content found.</p>", current_source_hash, None, "", "", []
|
| 377 |
+
return
|
| 378 |
except Exception as e:
|
| 379 |
+
yield f"<p>Error reading content: {e}</p>", None, None, "", "", []
|
| 380 |
+
return
|
| 381 |
|
| 382 |
vocab_list = []
|
| 383 |
+
stream_text = ""
|
| 384 |
for attempt in range(1, 4):
|
| 385 |
progress(0.2, desc=f"Extracting vocabulary (Attempt {attempt}/3)...")
|
| 386 |
+
for stream_t, v_list in extract_vocabulary(content_text, images, translit_lang, translit_format, target_lang, max_text_char):
|
| 387 |
+
stream_text = stream_t
|
| 388 |
+
if v_list is not None:
|
| 389 |
+
vocab_list = v_list
|
| 390 |
+
yield "", current_source_hash, None, stream_text, content_text, images
|
| 391 |
+
|
| 392 |
if vocab_list:
|
| 393 |
break
|
| 394 |
|
| 395 |
if not vocab_list:
|
| 396 |
+
yield "<p>Failed to extract or translate vocabulary after 3 attempts.</p>", current_source_hash, None, stream_text, content_text, images
|
| 397 |
+
return
|
| 398 |
|
| 399 |
progress(0.6, desc="Generating TTS audio...")
|
| 400 |
# Pre-generate TTS audio
|
|
|
|
| 405 |
korean += "."
|
| 406 |
|
| 407 |
try:
|
| 408 |
+
wav, dur = tts.synthesize(
|
| 409 |
+
korean, voice_style=voice_style, lang="ko",
|
| 410 |
+
total_steps=12,
|
| 411 |
+
speed=0.7,
|
| 412 |
+
)
|
| 413 |
|
| 414 |
# DEBUG: Save audio locally
|
| 415 |
wav_1d = wav.squeeze()
|
|
|
|
| 445 |
}}
|
| 446 |
.flashcard {{
|
| 447 |
width: 100%;
|
| 448 |
+
min-height: 400px;
|
| 449 |
+
display: grid;
|
| 450 |
transition: transform 0.6s cubic-bezier(0.4, 0.2, 0.2, 1);
|
| 451 |
transform-style: preserve-3d;
|
| 452 |
cursor: pointer;
|
|
|
|
| 455 |
transform: rotateY(180deg);
|
| 456 |
}}
|
| 457 |
.card-face {{
|
| 458 |
+
grid-area: 1 / 1;
|
| 459 |
width: 100%;
|
|
|
|
| 460 |
backface-visibility: hidden;
|
| 461 |
display: flex;
|
| 462 |
flex-direction: column;
|
|
|
|
| 564 |
font-size: 14px;
|
| 565 |
font-weight: 600;
|
| 566 |
}}
|
| 567 |
+
.copy-icon {{
|
| 568 |
+
cursor: pointer;
|
| 569 |
+
color: #a0a0a0;
|
| 570 |
+
font-size: 24px;
|
| 571 |
+
transition: color 0.2s, transform 0.2s;
|
| 572 |
+
}}
|
| 573 |
+
.copy-icon:hover {{
|
| 574 |
+
color: #7c3aed;
|
| 575 |
+
transform: scale(1.1);
|
| 576 |
+
}}
|
| 577 |
</style>
|
| 578 |
</head>
|
| 579 |
<body>
|
|
|
|
| 581 |
<div class="flashcard-container">
|
| 582 |
<div class="flashcard" id="card" onclick="flipCard()">
|
| 583 |
<div class="card-face card-front">
|
| 584 |
+
<div style="display: flex; align-items: center; justify-content: center; gap: 15px; margin-bottom: 20px; width: 100%;">
|
| 585 |
+
<div class="korean-text" id="front-text" style="margin-bottom: 0;"><i class="fi fi-rr-spinner-third fa-spin"></i> Loading...</div>
|
| 586 |
+
<i class="fi fi-rr-copy copy-icon" id="copy-ko" onclick="copyText('front-text', event, this)" title="Copy text" style="display:none;"></i>
|
| 587 |
+
</div>
|
| 588 |
<button class="audio-btn" onclick="playAudio(event)" id="audio-btn" style="display:none;"><i class="fi fi-rr-play-circle"></i> Play Audio</button>
|
| 589 |
<p style="margin-top:20px; color:#999; font-size:13px; display:flex; align-items:center; gap:5px;"><i class="fi fi-rr-rotate-right"></i> Click card to flip 🎯</p>
|
| 590 |
</div>
|
| 591 |
<div class="card-face card-back">
|
| 592 |
+
<div style="display: flex; align-items: center; justify-content: center; gap: 15px; margin-bottom: 5px; width: 100%;">
|
| 593 |
+
<div class="english-text" id="back-en" style="margin-bottom: 0;"></div>
|
| 594 |
+
<i class="fi fi-rr-copy copy-icon" id="copy-en" onclick="copyText('back-en', event, this)" title="Copy text" style="display:none;"></i>
|
| 595 |
+
</div>
|
| 596 |
<div class="translit-text" id="back-translit"></div>
|
| 597 |
<div class="explanation-text"><i class="fi fi-rr-lightbulb-on" style="color:#f1c40f;"></i> <span id="back-exp"></span></div>
|
| 598 |
</div>
|
|
|
|
| 615 |
document.getElementById('front-text').innerHTML = "No vocabulary found 😥";
|
| 616 |
document.getElementById('prev-btn').disabled = true;
|
| 617 |
document.getElementById('next-btn').disabled = true;
|
| 618 |
+
if(document.getElementById('copy-ko')) document.getElementById('copy-ko').style.display = 'none';
|
| 619 |
+
if(document.getElementById('copy-en')) document.getElementById('copy-en').style.display = 'none';
|
| 620 |
return;
|
| 621 |
}}
|
| 622 |
const card = cards[currentIndex];
|
|
|
|
| 625 |
document.getElementById('back-translit').innerText = card.transliteration ? `[${{card.transliteration}}]` : "";
|
| 626 |
document.getElementById('back-exp').innerText = card.explanation || "";
|
| 627 |
|
| 628 |
+
if(document.getElementById('copy-ko')) document.getElementById('copy-ko').style.display = 'block';
|
| 629 |
+
if(document.getElementById('copy-en')) document.getElementById('copy-en').style.display = 'block';
|
| 630 |
+
|
| 631 |
document.getElementById('prev-btn').disabled = currentIndex === 0;
|
| 632 |
document.getElementById('next-btn').disabled = currentIndex === cards.length - 1;
|
| 633 |
document.getElementById('progress-text').innerHTML = `📚 Card ${{currentIndex + 1}} of ${{cards.length}}`;
|
|
|
|
| 667 |
}}
|
| 668 |
}}
|
| 669 |
|
| 670 |
+
function copyText(elementId, e, iconEl) {{
|
| 671 |
+
e.stopPropagation();
|
| 672 |
+
const textToCopy = document.getElementById(elementId).innerText;
|
| 673 |
+
navigator.clipboard.writeText(textToCopy).then(() => {{
|
| 674 |
+
const oldClass = iconEl.className;
|
| 675 |
+
iconEl.className = "fi fi-rr-check copy-icon";
|
| 676 |
+
iconEl.style.color = "#27ae60";
|
| 677 |
+
setTimeout(() => {{
|
| 678 |
+
iconEl.className = oldClass;
|
| 679 |
+
iconEl.style.color = "";
|
| 680 |
+
}}, 1500);
|
| 681 |
+
}}).catch(err => console.log('Copy failed', err));
|
| 682 |
+
}}
|
| 683 |
+
|
| 684 |
window.onload = function() {{
|
| 685 |
updateCard();
|
| 686 |
}};
|
|
|
|
| 693 |
safe_srcdoc = html.escape(iframe_html)
|
| 694 |
|
| 695 |
# Return the iframe containing the whole SPA
|
| 696 |
+
yield f'<iframe srcdoc="{safe_srcdoc}" style="width: 100%; height: 650px; border: none; overflow-y: auto;"></iframe>', current_source_hash, vocab_list, stream_text, content_text, images
|
| 697 |
|
| 698 |
LANGUAGE_DATA = """Indo-European English, French, Portuguese, German, Romanian, Swedish, Danish, Bulgarian, Russian, Czech, Greek, Ukrainian, Spanish, Dutch, Slovak, Croatian, Polish, Lithuanian, Norwegian Bokmål, Norwegian Nynorsk, Persian, Slovenian, Gujarati, Latvian, Italian, Occitan, Nepali, Marathi, Belarusian, Serbian, Luxembourgish, Venetian, Assamese, Welsh, Silesian, Asturian, Chhattisgarhi, Awadhi, Maithili, Bhojpuri, Sindhi, Irish, Faroese, Hindi, Punjabi, Bengali, Oriya, Tajik, Eastern Yiddish, Lombard, Ligurian, Sicilian, Friulian, Sardinian, Galician, Catalan, Icelandic, Tosk Albanian, Limburgish, Dari, Afrikaans, Macedonian, Sinhala, Urdu, Magahi, Bosnian, Armenian, Latgalian, Scottish Gaelic, Central Kurdish, Northern Kurdish, Southern Pashto, Sanskrit, Dhundari, Marwari, Ahirani, Bagheli, Bagri, Bundeli, Braj, Kumaoni, Kashmiri
|
| 699 |
Sino-Tibetan Chinese (Simplified), Chinese (Traditional), Cantonese, Burmese, Standard Tibetan, Meitei
|
|
|
|
| 728 |
def create_demo():
|
| 729 |
example_pdf = get_example_pdf()
|
| 730 |
|
| 731 |
+
css = """
|
| 732 |
+
#stream_box .progress-text,
|
| 733 |
+
#stream_box .progress-level,
|
| 734 |
+
#stream_box .progress,
|
| 735 |
+
#stream_box .progress-container {
|
| 736 |
+
display: none !important;
|
| 737 |
+
}
|
| 738 |
+
"""
|
| 739 |
+
with gr.Blocks(title="LocalDuo", css=css) as demo:
|
| 740 |
+
gr.Markdown("# 🇰🇷✨ LocalDuo - Learn Korean from PDFs & Websites")
|
| 741 |
+
gr.Markdown("Enter a website URL 🌐 or upload a Korean book PDF 📄. The app uses a **Vision-Language Model (VLM)** 🧠 to extract vocabulary from text and images, and a **Text-to-Speech (TTS)** engine 🗣️ to generate pronunciation audio.")
|
| 742 |
|
| 743 |
with gr.Row():
|
| 744 |
with gr.Column(scale=1):
|
| 745 |
+
url_input = gr.Textbox(label="Enter a Website URL 🌐", placeholder="e.g. https://www.bbc.com/korean", value="https://www.bbc.com/korean")
|
| 746 |
+
pdf_input = gr.File(label="Or Upload Book PDF 📚", file_types=[".pdf"], value=example_pdf)
|
| 747 |
|
| 748 |
gr.Markdown("### ⚙️ Customization Settings")
|
| 749 |
translit_lang = gr.Dropdown(
|
|
|
|
| 757 |
choices=LANGUAGE_CHOICES,
|
| 758 |
value="Indo-European - English"
|
| 759 |
)
|
| 760 |
+
max_text_char_input = gr.Slider(minimum=1000, maximum=30000, step=1000, value=1500, label="Max Input Text Length (Characters)")
|
| 761 |
|
| 762 |
+
with gr.Row():
|
| 763 |
+
submit_btn = gr.Button("✨ Generate Flashcards ✨", variant="primary")
|
| 764 |
+
stop_btn = gr.Button("🛑 Stop Generation", variant="stop")
|
| 765 |
|
| 766 |
with gr.Column(scale=2):
|
| 767 |
output_html = gr.HTML(label="Flashcards will appear here")
|
| 768 |
+
stream_box = gr.Textbox(label="Live Model Generation 🧠", lines=10, max_lines=20, interactive=False, autoscroll=True, elem_id="stream_box")
|
| 769 |
+
|
| 770 |
+
with gr.Accordion("📄 Extracted Source Content", open=True):
|
| 771 |
+
extracted_text_box = gr.Textbox(label="Extracted Text", lines=10, max_lines=15, interactive=False)
|
| 772 |
+
extracted_images_gallery = gr.Gallery(label="Extracted Images", columns=4, height="auto", object_fit="contain")
|
| 773 |
|
| 774 |
+
last_source_state = gr.State(None)
|
| 775 |
+
last_korean_words_state = gr.State(None)
|
| 776 |
+
|
| 777 |
+
generate_event = submit_btn.click(
|
| 778 |
fn=process_pdf,
|
| 779 |
+
inputs=[pdf_input, url_input, translit_lang, translit_format, target_lang, max_text_char_input, last_source_state, last_korean_words_state],
|
| 780 |
+
outputs=[output_html, last_source_state, last_korean_words_state, stream_box, extracted_text_box, extracted_images_gallery]
|
| 781 |
+
)
|
| 782 |
+
|
| 783 |
+
stop_btn.click(fn=None, inputs=None, outputs=None, cancels=[generate_event])
|
| 784 |
+
|
| 785 |
+
# Force autoscroll using Custom JS
|
| 786 |
+
stream_box.change(
|
| 787 |
+
fn=None,
|
| 788 |
+
js="""
|
| 789 |
+
function() {
|
| 790 |
+
const ta = document.querySelector('#stream_box textarea');
|
| 791 |
+
if (ta) {
|
| 792 |
+
ta.scrollTop = ta.scrollHeight;
|
| 793 |
+
}
|
| 794 |
+
}
|
| 795 |
+
"""
|
| 796 |
)
|
| 797 |
return demo
|
| 798 |
|
|
|
|
| 813 |
# processor.chat_template = f.read()
|
| 814 |
# except Exception as e:
|
| 815 |
# print("Could not load custom chat template:", e)
|
| 816 |
+
processor.chat_template = """
|
| 817 |
+
{%- set image_count = namespace(value=0) %}
|
| 818 |
+
{%- set video_count = namespace(value=0) %}
|
| 819 |
+
{%- macro render_content(content, do_vision_count, is_system_content=false) %}
|
| 820 |
+
{%- if content is string %}
|
| 821 |
+
{{- content }}
|
| 822 |
+
{%- elif content is iterable and content is not mapping %}
|
| 823 |
+
{%- for item in content %}
|
| 824 |
+
{%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
|
| 825 |
+
{%- if is_system_content %}
|
| 826 |
+
{{- raise_exception('System message cannot contain images.') }}
|
| 827 |
+
{%- endif %}
|
| 828 |
+
{%- if do_vision_count %}
|
| 829 |
+
{%- set image_count.value = image_count.value + 1 %}
|
| 830 |
+
{%- endif %}
|
| 831 |
+
{%- if add_vision_id %}
|
| 832 |
+
{{- 'Picture ' ~ image_count.value ~ ': ' }}
|
| 833 |
+
{%- endif %}
|
| 834 |
+
{{- '<|vision_start|><|image_pad|><|vision_end|>' }}
|
| 835 |
+
{%- elif 'video' in item or item.type == 'video' %}
|
| 836 |
+
{%- if is_system_content %}
|
| 837 |
+
{{- raise_exception('System message cannot contain videos.') }}
|
| 838 |
+
{%- endif %}
|
| 839 |
+
{%- if do_vision_count %}
|
| 840 |
+
{%- set video_count.value = video_count.value + 1 %}
|
| 841 |
+
{%- endif %}
|
| 842 |
+
{%- if add_vision_id %}
|
| 843 |
+
{{- 'Video ' ~ video_count.value ~ ': ' }}
|
| 844 |
+
{%- endif %}
|
| 845 |
+
{{- '<|vision_start|><|video_pad|><|vision_end|>' }}
|
| 846 |
+
{%- elif 'text' in item %}
|
| 847 |
+
{{- item.text }}
|
| 848 |
+
{%- else %}
|
| 849 |
+
{{- raise_exception('Unexpected item type in content.') }}
|
| 850 |
+
{%- endif %}
|
| 851 |
+
{%- endfor %}
|
| 852 |
+
{%- elif content is none or content is undefined %}
|
| 853 |
+
{{- '' }}
|
| 854 |
+
{%- else %}
|
| 855 |
+
{{- raise_exception('Unexpected content type.') }}
|
| 856 |
+
{%- endif %}
|
| 857 |
+
{%- endmacro %}
|
| 858 |
+
{%- if not messages %}
|
| 859 |
+
{{- raise_exception('No messages provided.') }}
|
| 860 |
+
{%- endif %}
|
| 861 |
+
{%- if tools and tools is iterable and tools is not mapping %}
|
| 862 |
+
{{- '<|im_start|>system\n' }}
|
| 863 |
+
{{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
|
| 864 |
+
{%- for tool in tools %}
|
| 865 |
+
{{- "\n" }}
|
| 866 |
+
{{- tool | tojson }}
|
| 867 |
+
{%- endfor %}
|
| 868 |
+
{{- "\n</tools>" }}
|
| 869 |
+
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
|
| 870 |
+
{%- if messages[0].role == 'system' %}
|
| 871 |
+
{%- set content = render_content(messages[0].content, false, true)|trim %}
|
| 872 |
+
{%- if content %}
|
| 873 |
+
{{- '\n\n' + content }}
|
| 874 |
+
{%- endif %}
|
| 875 |
+
{%- endif %}
|
| 876 |
+
{{- '<|im_end|>\n' }}
|
| 877 |
+
{%- else %}
|
| 878 |
+
{%- if messages[0].role == 'system' %}
|
| 879 |
+
{%- set content = render_content(messages[0].content, false, true)|trim %}
|
| 880 |
+
{{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
|
| 881 |
+
{%- endif %}
|
| 882 |
+
{%- endif %}
|
| 883 |
+
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
|
| 884 |
+
{%- for message in messages[::-1] %}
|
| 885 |
+
{%- set index = (messages|length - 1) - loop.index0 %}
|
| 886 |
+
{%- if ns.multi_step_tool and message.role == "user" %}
|
| 887 |
+
{%- set content = render_content(message.content, false)|trim %}
|
| 888 |
+
{%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
|
| 889 |
+
{%- set ns.multi_step_tool = false %}
|
| 890 |
+
{%- set ns.last_query_index = index %}
|
| 891 |
+
{%- endif %}
|
| 892 |
+
{%- endif %}
|
| 893 |
+
{%- endfor %}
|
| 894 |
+
{%- if ns.multi_step_tool %}
|
| 895 |
+
{{- raise_exception('No user query found in messages.') }}
|
| 896 |
+
{%- endif %}
|
| 897 |
+
{%- for message in messages %}
|
| 898 |
+
{%- set content = render_content(message.content, true)|trim %}
|
| 899 |
+
{%- if message.role == "system" %}
|
| 900 |
+
{%- if not loop.first %}
|
| 901 |
+
{{- raise_exception('System message must be at the beginning.') }}
|
| 902 |
+
{%- endif %}
|
| 903 |
+
{%- elif message.role == "user" %}
|
| 904 |
+
{{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
|
| 905 |
+
{%- elif message.role == "assistant" %}
|
| 906 |
+
{%- set reasoning_content = '' %}
|
| 907 |
+
{%- if message.reasoning_content is string %}
|
| 908 |
+
{%- set reasoning_content = message.reasoning_content %}
|
| 909 |
+
{%- else %}
|
| 910 |
+
{%- if '</think>' in content %}
|
| 911 |
+
{%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
|
| 912 |
+
{%- set content = content.split('</think>')[-1].lstrip('\n') %}
|
| 913 |
+
{%- endif %}
|
| 914 |
+
{%- endif %}
|
| 915 |
+
{%- set reasoning_content = reasoning_content|trim %}
|
| 916 |
+
{%- if loop.index0 > ns.last_query_index %}
|
| 917 |
+
{{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
|
| 918 |
+
{%- else %}
|
| 919 |
+
{{- '<|im_start|>' + message.role + '\n' + content }}
|
| 920 |
+
{%- endif %}
|
| 921 |
+
{%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
|
| 922 |
+
{%- for tool_call in message.tool_calls %}
|
| 923 |
+
{%- if tool_call.function is defined %}
|
| 924 |
+
{%- set tool_call = tool_call.function %}
|
| 925 |
+
{%- endif %}
|
| 926 |
+
{%- if loop.first %}
|
| 927 |
+
{%- if content|trim %}
|
| 928 |
+
{{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 929 |
+
{%- else %}
|
| 930 |
+
{{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 931 |
+
{%- endif %}
|
| 932 |
+
{%- else %}
|
| 933 |
+
{{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 934 |
+
{%- endif %}
|
| 935 |
+
{%- if tool_call.arguments is defined %}
|
| 936 |
+
{%- for args_name, args_value in tool_call.arguments|items %}
|
| 937 |
+
{{- '<parameter=' + args_name + '>\n' }}
|
| 938 |
+
{%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
|
| 939 |
+
{{- args_value }}
|
| 940 |
+
{{- '\n</parameter>\n' }}
|
| 941 |
+
{%- endfor %}
|
| 942 |
+
{%- endif %}
|
| 943 |
+
{{- '</function>\n</tool_call>' }}
|
| 944 |
+
{%- endfor %}
|
| 945 |
+
{%- endif %}
|
| 946 |
+
{{- '<|im_end|>\n' }}
|
| 947 |
+
{%- elif message.role == "tool" %}
|
| 948 |
+
{%- if loop.previtem and loop.previtem.role != "tool" %}
|
| 949 |
+
{{- '<|im_start|>user' }}
|
| 950 |
+
{%- endif %}
|
| 951 |
+
{{- '\n<tool_response>\n' }}
|
| 952 |
+
{{- content }}
|
| 953 |
+
{{- '\n</tool_response>' }}
|
| 954 |
+
{%- if not loop.last and loop.nextitem.role != "tool" %}
|
| 955 |
+
{{- '<|im_end|>\n' }}
|
| 956 |
+
{%- elif loop.last %}
|
| 957 |
+
{{- '<|im_end|>\n' }}
|
| 958 |
+
{%- endif %}
|
| 959 |
+
{%- else %}
|
| 960 |
+
{{- raise_exception('Unexpected message role.') }}
|
| 961 |
+
{%- endif %}
|
| 962 |
+
{%- endfor %}
|
| 963 |
+
{%- if add_generation_prompt %}
|
| 964 |
+
{{- '<|im_start|>assistant\n' }}
|
| 965 |
+
{%- if enable_thinking is defined and enable_thinking is false %}
|
| 966 |
+
{{- '<think>\n\n</think>\n\n' }}
|
| 967 |
+
{%- else %}
|
| 968 |
+
{{- '<think>\n' }}
|
| 969 |
+
{%- endif %}
|
| 970 |
+
{%- endif %}
|
| 971 |
+
""".strip()
|
| 972 |
|
| 973 |
model = AutoModelForImageTextToText.from_pretrained(
|
| 974 |
model_id,
|