Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -7,26 +7,16 @@ import openai
|
|
| 7 |
import os
|
| 8 |
import random
|
| 9 |
import re
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
# nltk 데이터 다운로드 (최초 한 번 실행)
|
| 17 |
-
nltk.download('punkt')
|
| 18 |
-
|
| 19 |
-
# 로깅 설정
|
| 20 |
-
logging.basicConfig(
|
| 21 |
-
filename='youtube_script_extractor.log',
|
| 22 |
-
level=logging.DEBUG,
|
| 23 |
-
format='%(asctime)s - %(levelname)s - %(message)s'
|
| 24 |
-
)
|
| 25 |
|
| 26 |
def parse_api_response(response):
|
| 27 |
try:
|
| 28 |
if isinstance(response, str):
|
| 29 |
-
response =
|
| 30 |
if isinstance(response, list) and len(response) > 0:
|
| 31 |
response = response[0]
|
| 32 |
if not isinstance(response, dict):
|
|
@@ -35,29 +25,56 @@ def parse_api_response(response):
|
|
| 35 |
except Exception as e:
|
| 36 |
raise ValueError(f"API 응답 파싱 실패: {str(e)}")
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
def get_youtube_script(url):
|
| 39 |
logging.info(f"스크립트 추출 시작: URL = {url}")
|
|
|
|
| 40 |
client = Client("whispersound/YT_Ts_R")
|
|
|
|
| 41 |
try:
|
| 42 |
logging.debug("API 호출 시작")
|
| 43 |
result = client.predict(youtube_url=url, api_name="/predict")
|
| 44 |
logging.debug("API 호출 완료")
|
|
|
|
| 45 |
parsed_result = parse_api_response(result)
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
logging.info("스크립트 추출 완료")
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
"transcription": transcription
|
| 52 |
-
})
|
| 53 |
-
return title, script_json
|
| 54 |
except Exception as e:
|
| 55 |
error_msg = f"스크립트 추출 중 오류 발생: {str(e)}"
|
| 56 |
logging.exception(error_msg)
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
# OpenAI API 키 설정
|
| 60 |
-
openai.api_key = os.getenv("OPENAI_API_KEY")
|
| 61 |
|
| 62 |
def call_api(prompt, max_tokens, temperature, top_p):
|
| 63 |
try:
|
|
@@ -71,119 +88,125 @@ def call_api(prompt, max_tokens, temperature, top_p):
|
|
| 71 |
return response['choices'][0]['message']['content']
|
| 72 |
except Exception as e:
|
| 73 |
logging.exception("LLM API 호출 중 오류 발생")
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
def extract_video_id(url):
|
| 77 |
-
parsed_url = urllib.parse.urlparse(url)
|
| 78 |
-
if parsed_url.hostname in ('www.youtube.com', 'youtube.com'):
|
| 79 |
-
query_params = urllib.parse.parse_qs(parsed_url.query)
|
| 80 |
-
return query_params.get('v', [None])[0]
|
| 81 |
-
elif parsed_url.hostname == 'youtu.be':
|
| 82 |
-
return parsed_url.path[1:]
|
| 83 |
-
else:
|
| 84 |
-
return None
|
| 85 |
|
| 86 |
def summarize_section(section_text):
|
| 87 |
-
prompt = f"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
|
|
|
| 89 |
{section_text}
|
| 90 |
-
|
| 91 |
-
요약은 한국어로 간결하게 작성해 주세요.
|
| 92 |
"""
|
| 93 |
-
return call_api(prompt, max_tokens=
|
| 94 |
-
|
| 95 |
-
def
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
vectorizer = TfidfVectorizer().fit_transform(sentences)
|
| 106 |
-
vectors = vectorizer.toarray()
|
| 107 |
-
|
| 108 |
-
boundaries = [0]
|
| 109 |
-
threshold = 0.3
|
| 110 |
-
for i in range(1, len(sentences)):
|
| 111 |
-
similarity = cosine_similarity([vectors[i - 1]], [vectors[i]])[0][0]
|
| 112 |
-
if similarity < threshold:
|
| 113 |
-
boundaries.append(i)
|
| 114 |
-
boundaries.append(len(sentences))
|
| 115 |
-
|
| 116 |
-
sections = []
|
| 117 |
-
for i in range(len(boundaries) - 1):
|
| 118 |
-
start_idx = boundaries[i]
|
| 119 |
-
end_idx = boundaries[i + 1]
|
| 120 |
-
section_sentences = sentences[start_idx:end_idx]
|
| 121 |
-
section_text = ' '.join(section_sentences)
|
| 122 |
-
section_start_time = start_times[start_idx]
|
| 123 |
-
sections.append({
|
| 124 |
-
'text': section_text,
|
| 125 |
-
'start_time': section_start_time
|
| 126 |
-
})
|
| 127 |
-
return sections
|
| 128 |
-
|
| 129 |
-
def generate_summary(sections, url):
|
| 130 |
-
video_id = extract_video_id(url)
|
| 131 |
-
summary_html = "<h3>요약:</h3>"
|
| 132 |
-
for idx, section in enumerate(sections):
|
| 133 |
-
start_time = section['start_time']
|
| 134 |
-
hours = int(start_time // 3600)
|
| 135 |
-
minutes = int((start_time % 3600) // 60)
|
| 136 |
-
seconds = int(start_time % 60)
|
| 137 |
-
timestamp_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
| 138 |
-
timestamp_link = f"https://www.youtube.com/watch?v={video_id}&t={int(start_time)}s"
|
| 139 |
summary = summarize_section(section['text'])
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
with gr.Blocks() as demo:
|
| 147 |
gr.Markdown("## YouTube 스크립트 추출 및 요약 도구")
|
|
|
|
| 148 |
youtube_url_input = gr.Textbox(label="YouTube URL 입력")
|
| 149 |
analyze_button = gr.Button("분석하기")
|
| 150 |
script_output = gr.HTML(label="스크립트")
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
| 153 |
|
| 154 |
def extract_and_cache(url, cache):
|
| 155 |
if url == cache["url"]:
|
| 156 |
-
return cache["title"], cache["script"], cache
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
def display_script(title, script):
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
return script_html
|
| 164 |
|
| 165 |
-
def
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
try:
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
| 175 |
except Exception as e:
|
| 176 |
-
|
| 177 |
-
|
|
|
|
| 178 |
|
| 179 |
analyze_button.click(
|
| 180 |
-
|
| 181 |
-
inputs=[youtube_url_input, cached_data],
|
| 182 |
-
outputs=[script_output, cached_data]
|
| 183 |
-
).then(
|
| 184 |
-
update_summary,
|
| 185 |
-
inputs=[cached_data],
|
| 186 |
-
outputs=summary_output
|
| 187 |
)
|
| 188 |
|
| 189 |
demo.launch(share=True)
|
|
|
|
| 7 |
import os
|
| 8 |
import random
|
| 9 |
import re
|
| 10 |
+
|
| 11 |
+
logging.basicConfig(filename='youtube_script_extractor.log', level=logging.DEBUG,
|
| 12 |
+
format='%(asctime)s - %(levelname)s - %(message)s')
|
| 13 |
+
|
| 14 |
+
openai.api_key = os.getenv("OPENAI_API_KEY")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
def parse_api_response(response):
|
| 17 |
try:
|
| 18 |
if isinstance(response, str):
|
| 19 |
+
response = json.loads(response)
|
| 20 |
if isinstance(response, list) and len(response) > 0:
|
| 21 |
response = response[0]
|
| 22 |
if not isinstance(response, dict):
|
|
|
|
| 25 |
except Exception as e:
|
| 26 |
raise ValueError(f"API 응답 파싱 실패: {str(e)}")
|
| 27 |
|
| 28 |
+
def split_sentences(text):
|
| 29 |
+
sentences = re.split(r"(니다|에요|구나|해요|군요|겠어요|시오|해라|예요|아요|데요|대요|세요|어요|게요|구요|고요|나요|하죠)(?![\w])", text)
|
| 30 |
+
combined_sentences = []
|
| 31 |
+
current_sentence = ""
|
| 32 |
+
for i in range(0, len(sentences), 2):
|
| 33 |
+
if i + 1 < len(sentences):
|
| 34 |
+
sentence = sentences[i] + sentences[i + 1]
|
| 35 |
+
else:
|
| 36 |
+
sentence = sentences[i]
|
| 37 |
+
if len(current_sentence) + len(sentence) > 100:
|
| 38 |
+
combined_sentences.append(current_sentence.strip())
|
| 39 |
+
current_sentence = sentence.strip()
|
| 40 |
+
else:
|
| 41 |
+
current_sentence += sentence
|
| 42 |
+
if sentence.endswith(('.', '?', '!')):
|
| 43 |
+
combined_sentences.append(current_sentence.strip())
|
| 44 |
+
current_sentence = ""
|
| 45 |
+
if current_sentence:
|
| 46 |
+
combined_sentences.append(current_sentence.strip())
|
| 47 |
+
return combined_sentences
|
| 48 |
+
|
| 49 |
def get_youtube_script(url):
|
| 50 |
logging.info(f"스크립트 추출 시작: URL = {url}")
|
| 51 |
+
|
| 52 |
client = Client("whispersound/YT_Ts_R")
|
| 53 |
+
|
| 54 |
try:
|
| 55 |
logging.debug("API 호출 시작")
|
| 56 |
result = client.predict(youtube_url=url, api_name="/predict")
|
| 57 |
logging.debug("API 호출 완료")
|
| 58 |
+
|
| 59 |
parsed_result = parse_api_response(result)
|
| 60 |
+
|
| 61 |
+
if 'data' not in parsed_result or not parsed_result['data']:
|
| 62 |
+
raise ValueError("API 응답에 유효한 데이터가 없습니다.")
|
| 63 |
+
|
| 64 |
+
title = parsed_result["data"][0].get("title", "제목 없음")
|
| 65 |
+
transcription_text = parsed_result["data"][0].get("transcriptionAsText", "")
|
| 66 |
+
sections = parsed_result["data"][0].get("sections", [])
|
| 67 |
+
|
| 68 |
+
if not transcription_text:
|
| 69 |
+
raise ValueError("추출된 스크립트가 없습니다.")
|
| 70 |
+
|
| 71 |
logging.info("스크립트 추출 완료")
|
| 72 |
+
return title, transcription_text, sections
|
| 73 |
+
|
|
|
|
|
|
|
|
|
|
| 74 |
except Exception as e:
|
| 75 |
error_msg = f"스크립트 추출 중 오류 발생: {str(e)}"
|
| 76 |
logging.exception(error_msg)
|
| 77 |
+
raise
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
def call_api(prompt, max_tokens, temperature, top_p):
|
| 80 |
try:
|
|
|
|
| 88 |
return response['choices'][0]['message']['content']
|
| 89 |
except Exception as e:
|
| 90 |
logging.exception("LLM API 호출 중 오류 발생")
|
| 91 |
+
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
def summarize_section(section_text):
|
| 94 |
+
prompt = f"""
|
| 95 |
+
다음 유튜브 대본 섹션의 핵심 내용을 간결하게 요약하세요:
|
| 96 |
+
1. 한글로 작성하세요.
|
| 97 |
+
2. 주요 논점과 중요한 세부사항을 포함하세요.
|
| 98 |
+
3. 요약은 2-3문장으로 제한하세요.
|
| 99 |
|
| 100 |
+
섹션 내용:
|
| 101 |
{section_text}
|
|
|
|
|
|
|
| 102 |
"""
|
| 103 |
+
return call_api(prompt, max_tokens=150, temperature=0.3, top_p=0.9)
|
| 104 |
+
|
| 105 |
+
def format_time(seconds):
|
| 106 |
+
minutes, seconds = divmod(seconds, 60)
|
| 107 |
+
hours, minutes = divmod(minutes, 60)
|
| 108 |
+
return f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}"
|
| 109 |
+
|
| 110 |
+
def generate_timeline_summary(sections):
|
| 111 |
+
timeline_summary = ""
|
| 112 |
+
for i, section in enumerate(sections, 1):
|
| 113 |
+
start_time = format_time(section['start_time'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
summary = summarize_section(section['text'])
|
| 115 |
+
timeline_summary += f"{start_time} {i}. {summary}\n\n"
|
| 116 |
+
return timeline_summary
|
| 117 |
+
|
| 118 |
+
def summarize_text(text):
|
| 119 |
+
prompt = f"""
|
| 120 |
+
1. 다음 주어지는 유튜브 대본의 핵심 주제와 모든 주요 내용을 상세하게 요약하라
|
| 121 |
+
2. 반드시 한글로 작성하라
|
| 122 |
+
3. 요약문만으로도 영상을 직접 시청한 것과 동일한 수준으로 내용을 이해할 수 있도록 상세히 작성
|
| 123 |
+
4. 글을 너무 압축하거나 함축하지 말고, 중요한 내용과 세부사항을 모두 포함
|
| 124 |
+
5. 반드시 대본의 흐름과 논리 구조를 유지
|
| 125 |
+
6. 반드시 시간 순서나 사건의 전개 과정을 명확하게 반영
|
| 126 |
+
7. 등장인물, 장소, 사건 등 중요한 요소를 정확하게 작성
|
| 127 |
+
8. 대본에서 전달하는 감정이나 분위기도 포함
|
| 128 |
+
9. 반드시 기술적 용어나 전문 용어가 있을 경우, 이를 정확하게 사용
|
| 129 |
+
10. 대본의 목적이나 의도를 파악하고, 이를 요약에 반드시 반영
|
| 130 |
+
11. 전체글을 보고
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
이 프롬프트가 도움이 되시길 바랍니다.
|
| 135 |
+
\n\n
|
| 136 |
+
{text}"""
|
| 137 |
+
|
| 138 |
+
return call_api(prompt, max_tokens=10000, temperature=0.3, top_p=0.9)
|
| 139 |
|
| 140 |
with gr.Blocks() as demo:
|
| 141 |
gr.Markdown("## YouTube 스크립트 추출 및 요약 도구")
|
| 142 |
+
|
| 143 |
youtube_url_input = gr.Textbox(label="YouTube URL 입력")
|
| 144 |
analyze_button = gr.Button("분석하기")
|
| 145 |
script_output = gr.HTML(label="스크립트")
|
| 146 |
+
timeline_output = gr.HTML(label="타임라인 요약")
|
| 147 |
+
summary_output = gr.HTML(label="전체 요약")
|
| 148 |
+
|
| 149 |
+
cached_data = gr.State({"url": "", "title": "", "script": "", "sections": []})
|
| 150 |
|
| 151 |
def extract_and_cache(url, cache):
|
| 152 |
if url == cache["url"]:
|
| 153 |
+
return cache["title"], cache["script"], cache["sections"], cache
|
| 154 |
+
|
| 155 |
+
try:
|
| 156 |
+
title, script, sections = get_youtube_script(url)
|
| 157 |
+
new_cache = {"url": url, "title": title, "script": script, "sections": sections}
|
| 158 |
+
return title, script, sections, new_cache
|
| 159 |
+
except Exception as e:
|
| 160 |
+
logging.exception("데이터 추출 중 오류 발생")
|
| 161 |
+
raise gr.Error(f"스크립트 추출 실패: {str(e)}")
|
| 162 |
|
| 163 |
def display_script(title, script):
|
| 164 |
+
formatted_script = "\n".join(split_sentences(script))
|
| 165 |
+
script_html = f"""<h2 style='font-size:24px;'>{title}</h2>
|
| 166 |
+
<details>
|
| 167 |
+
<summary><h3>원문 스크립트 (클릭하여 펼치기)</h3></summary>
|
| 168 |
+
<div style="white-space: pre-wrap;">{formatted_script}</div>
|
| 169 |
+
</details>"""
|
| 170 |
return script_html
|
| 171 |
|
| 172 |
+
def display_timeline(sections):
|
| 173 |
+
timeline_summary = generate_timeline_summary(sections)
|
| 174 |
+
timeline_html = f"""
|
| 175 |
+
<h3>타임라인 요약:</h3>
|
| 176 |
+
<div style="white-space: pre-wrap; max-height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px;">
|
| 177 |
+
{timeline_summary}
|
| 178 |
+
</div>
|
| 179 |
+
"""
|
| 180 |
+
return timeline_html
|
| 181 |
+
|
| 182 |
+
def generate_summary(script):
|
| 183 |
+
summary = summarize_text(script)
|
| 184 |
+
summary_html = f"""
|
| 185 |
+
<h3>전체 요약:</h3>
|
| 186 |
+
<div style="white-space: pre-wrap; max-height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px;">
|
| 187 |
+
{summary}
|
| 188 |
+
</div>
|
| 189 |
+
"""
|
| 190 |
+
return summary_html
|
| 191 |
+
|
| 192 |
+
def analyze(url, cache):
|
| 193 |
try:
|
| 194 |
+
title, script, sections, new_cache = extract_and_cache(url, cache)
|
| 195 |
+
script_html = display_script(title, script)
|
| 196 |
+
timeline_html = display_timeline(sections)
|
| 197 |
+
summary_html = generate_summary(script)
|
| 198 |
+
return script_html, timeline_html, summary_html, new_cache
|
| 199 |
+
except gr.Error as e:
|
| 200 |
+
return str(e), "", "", cache
|
| 201 |
except Exception as e:
|
| 202 |
+
error_msg = f"처리 중 오류 발생: {str(e)}"
|
| 203 |
+
logging.exception(error_msg)
|
| 204 |
+
return error_msg, "", "", cache
|
| 205 |
|
| 206 |
analyze_button.click(
|
| 207 |
+
analyze,
|
| 208 |
+
inputs=[youtube_url_input, cached_data],
|
| 209 |
+
outputs=[script_output, timeline_output, summary_output, cached_data]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
)
|
| 211 |
|
| 212 |
demo.launch(share=True)
|