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,
|