AIRider commited on
Commit
d3f7e6f
·
verified ·
1 Parent(s): d3555b8

Update app.py

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