Spaces:
Sleeping
Sleeping
function changeImage(direction, count, galleryIndex) {
Browse files
app.py
CHANGED
|
@@ -437,7 +437,6 @@ def generate_transcription(video_id):
|
|
| 437 |
|
| 438 |
return transcription
|
| 439 |
|
| 440 |
-
|
| 441 |
def process_transcript_and_screenshots(video_id):
|
| 442 |
print("====process_transcript_and_screenshots====")
|
| 443 |
|
|
@@ -611,6 +610,9 @@ def process_youtube_link(password, link):
|
|
| 611 |
formatted_transcript_json = json.dumps(formatted_transcript, ensure_ascii=False, indent=2)
|
| 612 |
summary_json = get_video_id_summary(video_id, formatted_simple_transcript, source)
|
| 613 |
summary = summary_json["summary"]
|
|
|
|
|
|
|
|
|
|
| 614 |
html_content = format_transcript_to_html(formatted_transcript)
|
| 615 |
simple_html_content = format_simple_transcript_to_html(formatted_simple_transcript)
|
| 616 |
first_image = formatted_transcript[0]['screenshot_path']
|
|
@@ -632,6 +634,7 @@ def process_youtube_link(password, link):
|
|
| 632 |
questions[2] if len(questions) > 2 else "", \
|
| 633 |
formatted_transcript_json, \
|
| 634 |
summary, \
|
|
|
|
| 635 |
mind_map, \
|
| 636 |
mind_map_html, \
|
| 637 |
html_content, \
|
|
@@ -1057,6 +1060,169 @@ def change_questions(password, df_string):
|
|
| 1057 |
print("=====get_questions=====")
|
| 1058 |
return q1, q2, q3
|
| 1059 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1060 |
# ---- LLM CRUD ----
|
| 1061 |
def enable_edit_mode():
|
| 1062 |
return gr.update(interactive=True)
|
|
@@ -1545,6 +1711,43 @@ HEAD = """
|
|
| 1545 |
});
|
| 1546 |
}
|
| 1547 |
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1548 |
"""
|
| 1549 |
|
| 1550 |
with gr.Blocks(theme=gr.themes.Base(primary_hue=gr.themes.colors.orange, secondary_hue=gr.themes.colors.amber, text_size = gr.themes.sizes.text_lg), head=HEAD) as demo:
|
|
@@ -1615,7 +1818,10 @@ with gr.Blocks(theme=gr.themes.Base(primary_hue=gr.themes.colors.orange, seconda
|
|
| 1615 |
summary_delete_button = gr.Button("刪除", size="sm", variant="primary")
|
| 1616 |
summary_create_button = gr.Button("建立", size="sm", variant="primary")
|
| 1617 |
with gr.Row():
|
| 1618 |
-
df_summarise = gr.Textbox(container=True, show_copy_button=True, lines=40, show_label=False)
|
|
|
|
|
|
|
|
|
|
| 1619 |
with gr.Tab("教學備課"):
|
| 1620 |
with gr.Row():
|
| 1621 |
content_subject = gr.Dropdown(label="選擇主題", choices=["數學", "自然", "國文", "英文", "社會","物理", "化學", "生物", "地理", "歷史", "公民"], value="", visible=False)
|
|
@@ -1796,7 +2002,8 @@ with gr.Blocks(theme=gr.themes.Base(primary_hue=gr.themes.colors.orange, seconda
|
|
| 1796 |
btn_2,
|
| 1797 |
btn_3,
|
| 1798 |
df_string_output,
|
| 1799 |
-
df_summarise,
|
|
|
|
| 1800 |
mind_map,
|
| 1801 |
mind_map_html,
|
| 1802 |
transcript_html,
|
|
|
|
| 437 |
|
| 438 |
return transcription
|
| 439 |
|
|
|
|
| 440 |
def process_transcript_and_screenshots(video_id):
|
| 441 |
print("====process_transcript_and_screenshots====")
|
| 442 |
|
|
|
|
| 610 |
formatted_transcript_json = json.dumps(formatted_transcript, ensure_ascii=False, indent=2)
|
| 611 |
summary_json = get_video_id_summary(video_id, formatted_simple_transcript, source)
|
| 612 |
summary = summary_json["summary"]
|
| 613 |
+
key_moments_json = get_key_moments(video_id, formatted_simple_transcript, formatted_transcript, source)
|
| 614 |
+
key_moments = key_moments_json["key_moments"]
|
| 615 |
+
key_moments_html = get_key_moments_html(key_moments)
|
| 616 |
html_content = format_transcript_to_html(formatted_transcript)
|
| 617 |
simple_html_content = format_simple_transcript_to_html(formatted_simple_transcript)
|
| 618 |
first_image = formatted_transcript[0]['screenshot_path']
|
|
|
|
| 634 |
questions[2] if len(questions) > 2 else "", \
|
| 635 |
formatted_transcript_json, \
|
| 636 |
summary, \
|
| 637 |
+
key_moments_html, \
|
| 638 |
mind_map, \
|
| 639 |
mind_map_html, \
|
| 640 |
html_content, \
|
|
|
|
| 1060 |
print("=====get_questions=====")
|
| 1061 |
return q1, q2, q3
|
| 1062 |
|
| 1063 |
+
# 「關鍵時刻」另外獨立成一個 tab,時間戳記和文字的下方附上對應的截圖,重點摘要的「關鍵時刻」加上截圖資訊
|
| 1064 |
+
def get_key_moments(video_id, formatted_simple_transcript, formatted_transcript, source):
|
| 1065 |
+
if source == "gcs":
|
| 1066 |
+
print("===get_key_moments on gcs===")
|
| 1067 |
+
gcs_client = GCS_CLIENT
|
| 1068 |
+
bucket_name = 'video_ai_assistant'
|
| 1069 |
+
file_name = f'{video_id}_key_moments.json'
|
| 1070 |
+
blob_name = f"{video_id}/{file_name}"
|
| 1071 |
+
# 检查檔案是否存在
|
| 1072 |
+
is_key_moments_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
|
| 1073 |
+
if not is_key_moments_exists:
|
| 1074 |
+
key_moments = generate_key_moments(formatted_simple_transcript, formatted_transcript)
|
| 1075 |
+
key_moments_json = {"key_moments": key_moments}
|
| 1076 |
+
key_moments_text = json.dumps(key_moments_json, ensure_ascii=False, indent=2)
|
| 1077 |
+
upload_file_to_gcs_with_json_string(gcs_client, bucket_name, blob_name, key_moments_text)
|
| 1078 |
+
print("key_moments已上傳到GCS")
|
| 1079 |
+
else:
|
| 1080 |
+
# key_moments已存在,下载内容
|
| 1081 |
+
print("key_moments已存在于GCS中")
|
| 1082 |
+
key_moments_text = download_blob_to_string(gcs_client, bucket_name, blob_name)
|
| 1083 |
+
key_moments_json = json.loads(key_moments_text)
|
| 1084 |
+
|
| 1085 |
+
elif source == "drive":
|
| 1086 |
+
print("===get_key_moments on drive===")
|
| 1087 |
+
service = init_drive_service()
|
| 1088 |
+
parent_folder_id = '1GgI4YVs0KckwStVQkLa1NZ8IpaEMurkL'
|
| 1089 |
+
folder_id = create_folder_if_not_exists(service, video_id, parent_folder_id)
|
| 1090 |
+
file_name = f'{video_id}_key_moments.json'
|
| 1091 |
+
|
| 1092 |
+
# 检查檔案是否存在
|
| 1093 |
+
exists, file_id = check_file_exists(service, folder_id, file_name)
|
| 1094 |
+
if not exists:
|
| 1095 |
+
key_moments = generate_key_moments(formatted_simple_transcript, formatted_transcript)
|
| 1096 |
+
key_moments_json = {"key_moments": key_moments}
|
| 1097 |
+
key_moments_text = json.dumps(key_moments_json, ensure_ascii=False, indent=2)
|
| 1098 |
+
upload_content_directly(service, file_name, folder_id, key_moments_text)
|
| 1099 |
+
print("key_moments已上傳到Google Drive")
|
| 1100 |
+
else:
|
| 1101 |
+
# key_moments已存在,下载内容
|
| 1102 |
+
print("key_moments已存在于Google Drive中")
|
| 1103 |
+
key_moments_text = download_file_as_string(service, file_id)
|
| 1104 |
+
key_moments_json = json.loads(key_moments_text)
|
| 1105 |
+
|
| 1106 |
+
return key_moments_json
|
| 1107 |
+
|
| 1108 |
+
def generate_key_moments(formatted_simple_transcript, formatted_transcript):
|
| 1109 |
+
# 使用 OpenAI 生成基于上传数据的问题
|
| 1110 |
+
sys_content = "你是一個擅長資料分析跟影片教學的老師,user 為學生,請精讀資料文本,自行判斷資料的種類,使用 zh-TW"
|
| 1111 |
+
user_content = f"""
|
| 1112 |
+
請根據 {formatted_simple_transcript} 文本,提取出重點摘要,並給出對應的時間軸
|
| 1113 |
+
重點摘要的「關鍵時刻」加上截圖資訊
|
| 1114 |
+
1. 小範圍切出不同段落的相對應時間軸的重點摘要,
|
| 1115 |
+
2. 每一小段最多不超過 1/5 的總內容(例如五分鐘的影片就一段不超過一分鐘,10分鐘就一段最多兩分鐘)
|
| 1116 |
+
3. 注意不要遺漏任何一段時間軸的內容 從零秒開始
|
| 1117 |
+
4. 如果頭尾的情節不是重點,就併入到附近的段落,特別是打招呼或是介紹人物就是不重要的情節
|
| 1118 |
+
以這種方式分析整個文本,從零秒開始分析,直到結束。這很重要
|
| 1119 |
+
|
| 1120 |
+
並用 JSON 格式返回 key_moments:[{{
|
| 1121 |
+
"start": "00:00",
|
| 1122 |
+
"end": "00:00",
|
| 1123 |
+
"text": "逐字稿的重點摘要",
|
| 1124 |
+
"transcript": "逐字稿的集合(要有合理的標點符號)",
|
| 1125 |
+
"images": 截圖的連結們 list
|
| 1126 |
+
}}]
|
| 1127 |
+
"""
|
| 1128 |
+
messages = [
|
| 1129 |
+
{"role": "system", "content": sys_content},
|
| 1130 |
+
{"role": "user", "content": user_content}
|
| 1131 |
+
]
|
| 1132 |
+
response_format = { "type": "json_object" }
|
| 1133 |
+
|
| 1134 |
+
request_payload = {
|
| 1135 |
+
"model": "gpt-4-1106-preview",
|
| 1136 |
+
"messages": messages,
|
| 1137 |
+
"max_tokens": 4000,
|
| 1138 |
+
"response_format": response_format
|
| 1139 |
+
}
|
| 1140 |
+
|
| 1141 |
+
response = OPEN_AI_CLIENT.chat.completions.create(**request_payload)
|
| 1142 |
+
key_moments = json.loads(response.choices[0].message.content)["key_moments"]
|
| 1143 |
+
print("=====key_moments=====")
|
| 1144 |
+
print(key_moments)
|
| 1145 |
+
print("=====key_moments=====")
|
| 1146 |
+
image_links = {entry['start_time']: entry['screenshot_path'] for entry in formatted_transcript}
|
| 1147 |
+
for moment in key_moments:
|
| 1148 |
+
start_time = moment['start']
|
| 1149 |
+
end_time = moment['end']
|
| 1150 |
+
moment_images = [image_links[time] for time in image_links if start_time <= time <= end_time]
|
| 1151 |
+
moment['images'] = moment_images
|
| 1152 |
+
|
| 1153 |
+
return key_moments
|
| 1154 |
+
|
| 1155 |
+
def get_key_moments_html(key_moments):
|
| 1156 |
+
"""
|
| 1157 |
+
Generates HTML for key moments with a left-side gallery and right-side text.
|
| 1158 |
+
"""
|
| 1159 |
+
css = """
|
| 1160 |
+
<style>
|
| 1161 |
+
/* Existing CSS from your sample ... */
|
| 1162 |
+
|
| 1163 |
+
.gallery-container {
|
| 1164 |
+
display: flex;
|
| 1165 |
+
align-items: center;
|
| 1166 |
+
margin-bottom: 20px;
|
| 1167 |
+
}
|
| 1168 |
+
.image-container {
|
| 1169 |
+
position: relative;
|
| 1170 |
+
max-height: 350px;
|
| 1171 |
+
flex: 1;
|
| 1172 |
+
}
|
| 1173 |
+
.image-container img {
|
| 1174 |
+
max-height: 350px;
|
| 1175 |
+
display: block;
|
| 1176 |
+
margin: 0 auto; /* Center the image */
|
| 1177 |
+
}
|
| 1178 |
+
.text-content {
|
| 1179 |
+
flex: 2;
|
| 1180 |
+
margin-left: 20px;
|
| 1181 |
+
}
|
| 1182 |
+
.arrow {
|
| 1183 |
+
cursor: pointer;
|
| 1184 |
+
user-select: none;
|
| 1185 |
+
position: absolute;
|
| 1186 |
+
top: 50%;
|
| 1187 |
+
transform: translateY(-50%);
|
| 1188 |
+
background-color: rgba(255, 255, 255, 0.8);
|
| 1189 |
+
border: none;
|
| 1190 |
+
padding: 10px;
|
| 1191 |
+
font-size: 24px;
|
| 1192 |
+
z-index: 10;
|
| 1193 |
+
}
|
| 1194 |
+
.arrow-prev { left: 0; }
|
| 1195 |
+
.arrow-next { right: 0; }
|
| 1196 |
+
</style>
|
| 1197 |
+
"""
|
| 1198 |
+
|
| 1199 |
+
key_moments_html = "" + css
|
| 1200 |
+
|
| 1201 |
+
for i, moment in enumerate(key_moments):
|
| 1202 |
+
start_time = moment['start']
|
| 1203 |
+
end_time = moment['end']
|
| 1204 |
+
text = moment['text']
|
| 1205 |
+
transcript = moment['transcript']
|
| 1206 |
+
images = moment['images']
|
| 1207 |
+
image_elements = "".join([f'<img src="{img}" alt="Image {idx}" class="slide-image slide-image-{i}-{idx}" style="display: {"" if idx == 0 else "none"};" />' for idx, img in enumerate(images)])
|
| 1208 |
+
|
| 1209 |
+
key_moments_html += f"""
|
| 1210 |
+
<div class="gallery-container">
|
| 1211 |
+
<div class="image-container">
|
| 1212 |
+
<button class="arrow arrow-prev" onclick="changeImage(-1, {len(images)}, {i})">❮</button>
|
| 1213 |
+
{image_elements}
|
| 1214 |
+
<button class="arrow arrow-next" onclick="changeImage(1, {len(images)}, {i})">❯</button>
|
| 1215 |
+
</div>
|
| 1216 |
+
<div class="text-content">
|
| 1217 |
+
<h3>{start_time} - {end_time}</h3>
|
| 1218 |
+
<p><strong>摘要:</strong> {text}</p>
|
| 1219 |
+
<p><strong>逐字稿:</strong> {transcript}</p>
|
| 1220 |
+
</div>
|
| 1221 |
+
</div>
|
| 1222 |
+
"""
|
| 1223 |
+
|
| 1224 |
+
return key_moments_html
|
| 1225 |
+
|
| 1226 |
# ---- LLM CRUD ----
|
| 1227 |
def enable_edit_mode():
|
| 1228 |
return gr.update(interactive=True)
|
|
|
|
| 1711 |
});
|
| 1712 |
}
|
| 1713 |
</script>
|
| 1714 |
+
|
| 1715 |
+
<script>
|
| 1716 |
+
function changeImage(direction, count, galleryIndex) {
|
| 1717 |
+
// Find the current visible image by iterating over possible indices
|
| 1718 |
+
var currentImage = null;
|
| 1719 |
+
var currentIndex = -1;
|
| 1720 |
+
for (var i = 0; i < count; i++) {
|
| 1721 |
+
var img = document.querySelector('.slide-image-' + galleryIndex + '-' + i);
|
| 1722 |
+
if (img && img.style.display !== 'none') {
|
| 1723 |
+
currentImage = img;
|
| 1724 |
+
currentIndex = i;
|
| 1725 |
+
break;
|
| 1726 |
+
}
|
| 1727 |
+
}
|
| 1728 |
+
|
| 1729 |
+
// If no current image is visible, show the first one and return
|
| 1730 |
+
if (currentImage === null) {
|
| 1731 |
+
document.querySelector('.slide-image-' + galleryIndex + '-0').style.display = 'block';
|
| 1732 |
+
console.error('No current image found for galleryIndex ' + galleryIndex + ', defaulting to first image.');
|
| 1733 |
+
return;
|
| 1734 |
+
}
|
| 1735 |
+
|
| 1736 |
+
// Hide the current image
|
| 1737 |
+
currentImage.style.display = 'none';
|
| 1738 |
+
|
| 1739 |
+
// Calculate the index of the next image to show
|
| 1740 |
+
var newIndex = (currentIndex + direction + count) % count;
|
| 1741 |
+
|
| 1742 |
+
// Select the next image and show it
|
| 1743 |
+
var nextImage = document.querySelector('.slide-image-' + galleryIndex + '-' + newIndex);
|
| 1744 |
+
if (nextImage) {
|
| 1745 |
+
nextImage.style.display = 'block';
|
| 1746 |
+
} else {
|
| 1747 |
+
console.error('No image found for galleryIndex ' + galleryIndex + ' and newIndex ' + newIndex);
|
| 1748 |
+
}
|
| 1749 |
+
}
|
| 1750 |
+
</script>
|
| 1751 |
"""
|
| 1752 |
|
| 1753 |
with gr.Blocks(theme=gr.themes.Base(primary_hue=gr.themes.colors.orange, secondary_hue=gr.themes.colors.amber, text_size = gr.themes.sizes.text_lg), head=HEAD) as demo:
|
|
|
|
| 1818 |
summary_delete_button = gr.Button("刪除", size="sm", variant="primary")
|
| 1819 |
summary_create_button = gr.Button("建立", size="sm", variant="primary")
|
| 1820 |
with gr.Row():
|
| 1821 |
+
df_summarise = gr.Textbox(container=True, show_copy_button=True, lines=40, show_label=False)
|
| 1822 |
+
with gr.Tab("關鍵時刻"):
|
| 1823 |
+
with gr.Row():
|
| 1824 |
+
key_moments_html = gr.HTML(value="")
|
| 1825 |
with gr.Tab("教學備課"):
|
| 1826 |
with gr.Row():
|
| 1827 |
content_subject = gr.Dropdown(label="選擇主題", choices=["數學", "自然", "國文", "英文", "社會","物理", "化學", "生物", "地理", "歷史", "公民"], value="", visible=False)
|
|
|
|
| 2002 |
btn_2,
|
| 2003 |
btn_3,
|
| 2004 |
df_string_output,
|
| 2005 |
+
df_summarise,
|
| 2006 |
+
key_moments_html,
|
| 2007 |
mind_map,
|
| 2008 |
mind_map_html,
|
| 2009 |
transcript_html,
|