Spaces:
Sleeping
Sleeping
update
Browse files- .gitignore +1 -0
- README.md +1 -0
- app.py +1385 -429
- chatbot.py +155 -0
- educational_material.py +470 -0
- local_config_example.json +12 -0
- requirements.txt +7 -2
- storage_service.py +44 -0
.gitignore
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
local_config.json
|
README.md
CHANGED
@@ -9,4 +9,5 @@ app_file: app.py
|
|
9 |
pinned: false
|
10 |
---
|
11 |
|
|
|
12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
9 |
pinned: false
|
10 |
---
|
11 |
|
12 |
+
|
13 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
CHANGED
@@ -5,15 +5,24 @@ from bs4 import BeautifulSoup
|
|
5 |
from docx import Document
|
6 |
import os
|
7 |
from openai import OpenAI
|
8 |
-
import
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
from youtube_transcript_api import YouTubeTranscriptApi
|
11 |
from youtube_transcript_api._errors import NoTranscriptFound
|
12 |
-
|
13 |
|
14 |
from moviepy.editor import VideoFileClip
|
15 |
from pytube import YouTube
|
16 |
import os
|
|
|
|
|
|
|
|
|
17 |
|
18 |
from google.cloud import storage
|
19 |
from google.oauth2 import service_account
|
@@ -22,69 +31,68 @@ from googleapiclient.http import MediaFileUpload
|
|
22 |
from googleapiclient.http import MediaIoBaseDownload
|
23 |
from googleapiclient.http import MediaIoBaseUpload
|
24 |
|
25 |
-
import
|
26 |
-
import
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
|
53 |
-
OUTPUT_PATH = 'videos'
|
54 |
TRANSCRIPTS = []
|
55 |
CURRENT_INDEX = 0
|
56 |
VIDEO_ID = ""
|
57 |
|
58 |
-
OPEN_AI_KEY = os.getenv("OPEN_AI_KEY")
|
59 |
OPEN_AI_CLIENT = OpenAI(api_key=OPEN_AI_KEY)
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
bucket = gcs_client.bucket(bucket_name)
|
75 |
-
blob = bucket.blob(folder_name)
|
76 |
-
if not blob.exists():
|
77 |
-
blob.upload_from_string('', content_type='application/x-www-form-urlencoded;charset=UTF-8')
|
78 |
-
print(f"GCS Folder '{folder_name}' created.")
|
79 |
else:
|
80 |
-
|
81 |
-
|
82 |
-
def gcs_check_folder_exists(gcs_client, bucket_name, folder_name):
|
83 |
-
"""检查 GCS 存储桶中是否存在指定的文件夹"""
|
84 |
-
bucket = gcs_client.bucket(bucket_name)
|
85 |
-
blobs = list(bucket.list_blobs(prefix=folder_name))
|
86 |
-
return len(blobs) > 0
|
87 |
|
|
|
88 |
def gcs_check_file_exists(gcs_client, bucket_name, file_name):
|
89 |
"""
|
90 |
检查 GCS 存储桶中是否存在指定的文件
|
@@ -106,7 +114,7 @@ def upload_file_to_gcs_with_json_string(gcs_client, bucket_name, destination_blo
|
|
106 |
bucket = gcs_client.bucket(bucket_name)
|
107 |
blob = bucket.blob(destination_blob_name)
|
108 |
blob.upload_from_string(json_string)
|
109 |
-
print(f"JSON string uploaded to {destination_blob_name} in GCS.")
|
110 |
|
111 |
def download_blob_to_string(gcs_client, bucket_name, source_blob_name):
|
112 |
"""从 GCS 下载文件内容到字符串"""
|
@@ -167,6 +175,13 @@ def copy_file_from_drive_to_gcs(drive_service, gcs_client, file_id, bucket_name,
|
|
167 |
blob.upload_from_string(file_content)
|
168 |
print(f"File {file_id} copied to GCS at {gcs_destination_path}.")
|
169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
170 |
# # ====drive====初始化
|
171 |
def init_drive_service():
|
172 |
credentials_json_string = DRIVE_KEY
|
@@ -291,9 +306,10 @@ def update_file_on_drive(service, file_id, file_content):
|
|
291 |
|
292 |
print(f"文件已更新,文件ID: {updated_file['id']}")
|
293 |
|
294 |
-
|
295 |
-
|
296 |
-
|
|
|
297 |
# 读取文件
|
298 |
if file.name.endswith('.csv'):
|
299 |
df = pd.read_csv(file)
|
@@ -330,6 +346,8 @@ def docx_to_text(file):
|
|
330 |
doc = Document(file)
|
331 |
return "\n".join([para.text for para in doc.paragraphs])
|
332 |
|
|
|
|
|
333 |
def format_seconds_to_time(seconds):
|
334 |
"""将秒数格式化为 时:分:秒 的形式"""
|
335 |
hours = int(seconds // 3600)
|
@@ -360,6 +378,65 @@ def get_transcript(video_id):
|
|
360 |
continue # 當前語言的字幕沒有找到,繼續嘗試下一個語言
|
361 |
return None # 所有嘗試都失敗,返回None
|
362 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
363 |
def process_transcript_and_screenshots(video_id):
|
364 |
print("====process_transcript_and_screenshots====")
|
365 |
|
@@ -401,46 +478,33 @@ def process_transcript_and_screenshots(video_id):
|
|
401 |
updated_transcript_text = json.dumps(transcript, ensure_ascii=False, indent=2)
|
402 |
update_file_on_drive(service, file_id, updated_transcript_text)
|
403 |
print("逐字稿已更新,包括截图链接")
|
404 |
-
|
405 |
-
# init gcs client
|
406 |
-
gcs_client = init_gcs_client(GCS_KEY)
|
407 |
-
bucket_name = 'video_ai_assistant'
|
408 |
-
# 检查 folder 是否存在
|
409 |
-
is_gcs_exists = gcs_check_folder_exists(gcs_client, bucket_name, video_id)
|
410 |
-
if not is_gcs_exists:
|
411 |
-
gcs_create_bucket_folder_if_not_exists(gcs_client, bucket_name, video_id)
|
412 |
-
copy_all_files_from_drive_to_gcs(service, gcs_client, folder_id, bucket_name, video_id)
|
413 |
-
print("Drive file 已上传到GCS")
|
414 |
-
else:
|
415 |
-
print("GCS folder:{video_id} 已存在")
|
416 |
-
|
417 |
return transcript
|
418 |
|
419 |
def process_transcript_and_screenshots_on_gcs(video_id):
|
420 |
print("====process_transcript_and_screenshots_on_gcs====")
|
421 |
# GCS
|
422 |
-
gcs_client =
|
423 |
bucket_name = 'video_ai_assistant'
|
424 |
-
# 检查 folder 是否存在
|
425 |
-
# is_gcs_exists = gcs_check_folder_exists(gcs_client, bucket_name, video_id)
|
426 |
-
# if not is_gcs_exists:
|
427 |
-
# gcs_create_bucket_folder_if_not_exists(gcs_client, bucket_name, video_id)
|
428 |
-
# print("GCS folder:{video_id} 已创建")
|
429 |
-
# else:
|
430 |
-
# print("GCS folder:{video_id} 已存在")
|
431 |
-
|
432 |
# 逐字稿文件名
|
433 |
transcript_file_name = f'{video_id}_transcript.json'
|
434 |
transcript_blob_name = f"{video_id}/{transcript_file_name}"
|
435 |
# 检查逐字稿是否存在
|
436 |
-
is_transcript_exists =
|
437 |
if not is_transcript_exists:
|
438 |
# 从YouTube获取逐字稿并上传
|
439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
440 |
if transcript:
|
441 |
print("成功獲取字幕")
|
442 |
else:
|
443 |
print("沒有找到字幕")
|
|
|
|
|
444 |
transcript_text = json.dumps(transcript, ensure_ascii=False, indent=2)
|
445 |
upload_file_to_gcs_with_json_string(gcs_client, bucket_name, transcript_blob_name, transcript_text)
|
446 |
else:
|
@@ -456,34 +520,48 @@ def process_transcript_and_screenshots_on_gcs(video_id):
|
|
456 |
# get_mind_map(video_id, transcript_text, source)
|
457 |
# print("===確認其他衍生文件 end ===")
|
458 |
|
|
|
459 |
# 處理截圖
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
467 |
|
468 |
# 更新逐字稿文件
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
|
|
|
|
479 |
|
480 |
-
def process_youtube_link(link):
|
481 |
# 使用 YouTube API 获取逐字稿
|
482 |
# 假设您已经获取了 YouTube 视频的逐字稿并存储在变量 `transcript` 中
|
483 |
video_id = extract_youtube_id(link)
|
484 |
global VIDEO_ID
|
485 |
VIDEO_ID = video_id
|
486 |
-
download_youtube_video(video_id, output_path=OUTPUT_PATH)
|
487 |
|
488 |
try:
|
489 |
# transcript = process_transcript_and_screenshots(video_id)
|
@@ -501,8 +579,8 @@ def process_youtube_link(link):
|
|
501 |
start_time = format_seconds_to_time(entry['start'])
|
502 |
end_time = format_seconds_to_time(entry['start'] + entry['duration'])
|
503 |
embed_url = get_embedded_youtube_link(video_id, entry['start'])
|
504 |
-
|
505 |
-
img_file_id =""
|
506 |
# 先取消 Google Drive 的图片
|
507 |
# screenshot_path = f"https://lh3.googleusercontent.com/d/{img_file_id}=s4000"
|
508 |
screenshot_path = img_file_id
|
@@ -532,14 +610,22 @@ def process_youtube_link(link):
|
|
532 |
formatted_transcript_json = json.dumps(formatted_transcript, ensure_ascii=False, indent=2)
|
533 |
summary_json = get_video_id_summary(video_id, formatted_simple_transcript, source)
|
534 |
summary = summary_json["summary"]
|
|
|
|
|
|
|
535 |
html_content = format_transcript_to_html(formatted_transcript)
|
536 |
simple_html_content = format_simple_transcript_to_html(formatted_simple_transcript)
|
537 |
-
|
538 |
-
first_image = "https://www.nameslook.com/names/dfsadf-nameslook.png"
|
539 |
first_text = formatted_transcript[0]['text']
|
540 |
mind_map_json = get_mind_map(video_id, formatted_simple_transcript, source)
|
541 |
mind_map = mind_map_json["mind_map"]
|
542 |
mind_map_html = get_mind_map_html(mind_map)
|
|
|
|
|
|
|
|
|
|
|
543 |
|
544 |
# 确保返回与 UI 组件预期匹配的输出
|
545 |
return video_id, \
|
@@ -548,12 +634,16 @@ def process_youtube_link(link):
|
|
548 |
questions[2] if len(questions) > 2 else "", \
|
549 |
formatted_transcript_json, \
|
550 |
summary, \
|
|
|
551 |
mind_map, \
|
552 |
mind_map_html, \
|
553 |
html_content, \
|
554 |
simple_html_content, \
|
555 |
first_image, \
|
556 |
-
first_text,
|
|
|
|
|
|
|
557 |
|
558 |
def format_transcript_to_html(formatted_transcript):
|
559 |
html_content = ""
|
@@ -599,21 +689,105 @@ def screenshot_youtube_video(youtube_id, snapshot_sec):
|
|
599 |
|
600 |
return screenshot_path
|
601 |
|
|
|
|
|
602 |
def process_web_link(link):
|
603 |
# 抓取和解析网页内容
|
604 |
response = requests.get(link)
|
605 |
soup = BeautifulSoup(response.content, 'html.parser')
|
606 |
return soup.get_text()
|
607 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
608 |
def get_mind_map(video_id, df_string, source):
|
609 |
if source == "gcs":
|
610 |
print("===get_mind_map on gcs===")
|
611 |
-
gcs_client =
|
612 |
bucket_name = 'video_ai_assistant'
|
613 |
file_name = f'{video_id}_mind_map.json'
|
614 |
blob_name = f"{video_id}/{file_name}"
|
615 |
# 检查檔案是否存在
|
616 |
-
is_file_exists =
|
617 |
if not is_file_exists:
|
618 |
mind_map = generate_mind_map(df_string)
|
619 |
mind_map_json = {"mind_map": str(mind_map)}
|
@@ -691,12 +865,12 @@ def get_mind_map_html(mind_map):
|
|
691 |
def get_video_id_summary(video_id, df_string, source):
|
692 |
if source == "gcs":
|
693 |
print("===get_video_id_summary on gcs===")
|
694 |
-
gcs_client =
|
695 |
bucket_name = 'video_ai_assistant'
|
696 |
file_name = f'{video_id}_summary.json'
|
697 |
summary_file_blob_name = f"{video_id}/{file_name}"
|
698 |
# 检查 summary_file 是否存在
|
699 |
-
is_summary_file_exists =
|
700 |
if not is_summary_file_exists:
|
701 |
summary = generate_summarise(df_string)
|
702 |
summary_json = {"summary": str(summary)}
|
@@ -748,6 +922,7 @@ def generate_summarise(df_string):
|
|
748 |
如果是資料類型,請提估欄位敘述、資料樣態與資料分析,告訴學生這張表的意義,以及可能的結論與對應方式
|
749 |
|
750 |
如果是影片類型,請提估影片內容,告訴學生這部影片的意義,
|
|
|
751 |
小範圍切出不同段落的相對應時間軸的重點摘要,最多不超過五段
|
752 |
注意不要遺漏任何一段時間軸的內容
|
753 |
格式為 【start - end】: 摘要
|
@@ -756,9 +931,9 @@ def generate_summarise(df_string):
|
|
756 |
整體格式為:
|
757 |
🗂️ 1. 內容類型:?
|
758 |
📚 2. 整體摘要
|
759 |
-
🔖 3.
|
760 |
-
🔑 4.
|
761 |
-
💡 5.
|
762 |
❓ 6. 延伸小問題
|
763 |
"""
|
764 |
|
@@ -788,47 +963,16 @@ def generate_summarise(df_string):
|
|
788 |
|
789 |
return df_summarise
|
790 |
|
791 |
-
def generate_questions(df_string):
|
792 |
-
# 使用 OpenAI 生成基于上传数据的问题
|
793 |
-
|
794 |
-
sys_content = "你是一個擅長資料分析跟影片教學的老師,user 為學生,請精讀資料文本,自行判斷資料的種類,並用既有資料為本質猜測用戶可能會問的問題,使用 zh-TW"
|
795 |
-
user_content = f"請根據 {df_string} 生成三個問題,並用 JSON 格式返回 questions:[q1的敘述text, q2的敘述text, q3的敘述text]"
|
796 |
-
messages = [
|
797 |
-
{"role": "system", "content": sys_content},
|
798 |
-
{"role": "user", "content": user_content}
|
799 |
-
]
|
800 |
-
response_format = { "type": "json_object" }
|
801 |
-
|
802 |
-
print("=====messages=====")
|
803 |
-
print(messages)
|
804 |
-
print("=====messages=====")
|
805 |
-
|
806 |
-
|
807 |
-
request_payload = {
|
808 |
-
"model": "gpt-4-1106-preview",
|
809 |
-
"messages": messages,
|
810 |
-
"max_tokens": 4000,
|
811 |
-
"response_format": response_format
|
812 |
-
}
|
813 |
-
|
814 |
-
response = OPEN_AI_CLIENT.chat.completions.create(**request_payload)
|
815 |
-
questions = json.loads(response.choices[0].message.content)["questions"]
|
816 |
-
print("=====json_response=====")
|
817 |
-
print(questions)
|
818 |
-
print("=====json_response=====")
|
819 |
-
|
820 |
-
return questions
|
821 |
-
|
822 |
def get_questions(video_id, df_string, source="gcs"):
|
823 |
if source == "gcs":
|
824 |
# 去 gcs 確認是有有 video_id_questions.json
|
825 |
print("===get_questions on gcs===")
|
826 |
-
gcs_client =
|
827 |
bucket_name = 'video_ai_assistant'
|
828 |
file_name = f'{video_id}_questions.json'
|
829 |
blob_name = f"{video_id}/{file_name}"
|
830 |
# 检查檔案是否存在
|
831 |
-
is_questions_exists =
|
832 |
if not is_questions_exists:
|
833 |
questions = generate_questions(df_string)
|
834 |
questions_text = json.dumps(questions, ensure_ascii=False, indent=2)
|
@@ -871,7 +1015,40 @@ def get_questions(video_id, df_string, source="gcs"):
|
|
871 |
print("=====get_questions=====")
|
872 |
return q1, q2, q3
|
873 |
|
874 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
875 |
questions = generate_questions(df_string)
|
876 |
q1 = questions[0] if len(questions) > 0 else ""
|
877 |
q2 = questions[1] if len(questions) > 1 else ""
|
@@ -883,192 +1060,663 @@ def change_questions(df_string):
|
|
883 |
print("=====get_questions=====")
|
884 |
return q1, q2, q3
|
885 |
|
886 |
-
|
887 |
-
|
888 |
-
|
889 |
-
|
890 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
891 |
|
892 |
-
|
893 |
-
|
894 |
-
|
895 |
-
|
|
|
|
|
896 |
|
897 |
-
|
898 |
-
|
899 |
-
|
900 |
-
|
901 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
902 |
|
903 |
-
|
904 |
-
|
905 |
-
|
906 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
907 |
|
908 |
-
|
909 |
-
|
|
|
|
|
|
|
|
|
910 |
|
911 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
912 |
|
913 |
-
|
914 |
-
|
915 |
-
|
916 |
-
|
917 |
-
|
918 |
-
請用 {data} 為資料文本,自行判斷資料的種類,
|
919 |
-
並進行對話,使用 zh-TW
|
920 |
|
921 |
-
|
922 |
-
|
|
|
|
|
923 |
|
924 |
-
如果學生問了一些問題你無法判斷,請告訴學生你無法判斷,並建議學生可以問其他問題
|
925 |
-
或者你可以問學生一些問題,幫助學生更好的理解資料
|
926 |
|
927 |
-
|
928 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
929 |
|
930 |
-
|
931 |
-
|
932 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
933 |
|
934 |
-
#
|
935 |
-
|
936 |
-
|
937 |
-
|
938 |
-
|
939 |
-
|
940 |
-
|
941 |
-
|
942 |
-
|
943 |
-
|
944 |
-
|
945 |
-
|
946 |
-
|
947 |
-
|
948 |
-
|
949 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
950 |
|
951 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
952 |
request_payload = {
|
953 |
-
"model":
|
954 |
"messages": messages,
|
955 |
-
"max_tokens": 4000 #
|
956 |
}
|
957 |
-
|
958 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
959 |
|
960 |
-
|
961 |
-
|
962 |
-
|
963 |
-
|
964 |
-
|
965 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
966 |
|
967 |
-
|
968 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
969 |
|
970 |
-
def chat_with_youtube_transcript(youtube_id, thread_id, trascript, user_message, chat_history, socratic_mode=False):
|
971 |
# 先計算 user_message 是否超過 500 個字
|
972 |
if len(user_message) > 1500:
|
973 |
error_msg = "你的訊息太長了,請縮短訊息長度至五百字以內"
|
974 |
raise gr.Error(error_msg)
|
975 |
|
976 |
-
|
977 |
-
|
978 |
-
|
979 |
-
|
980 |
-
# instructions = f"""
|
981 |
-
# 你是一個擅長資料分析跟影片教學的老師,user 為學生
|
982 |
-
# 請根據 assistant beta 的上傳資料
|
983 |
-
# 如果 file 內有找到 file.content["{youtube_id}"] 為資料文本,自行判斷資料的種類,
|
984 |
-
# 如果沒有資料,請告訴用戶沒有逐字稿資料,但仍然可以進行對話,使用台灣人的口與表達,及繁體中文 zh-TW
|
985 |
-
# 請嚴格執行,只根據 file.content["{youtube_id}"] 為資料文本,沒有就是沒有資料,不要引用其他資料
|
986 |
-
|
987 |
-
# 如果是影片類型,不用解釋逐字稿格式,直接回答學生問題
|
988 |
-
# socratic_mode = {socratic_mode}
|
989 |
-
# 如果 socratic_mode = True,
|
990 |
-
# - 請用蘇格拉底式的提問方式,引導學生思考,並且給予學生一些提示
|
991 |
-
# - 不要直接給予答案,讓學生自己思考
|
992 |
-
# - 但可以給予一些提示跟引導,例如給予影片的時間軸,讓學生自己去找答案
|
993 |
-
# - 在你回答的開頭標註【蘇格拉底助教:{youtube_id} 】
|
994 |
-
# 如果 socratic_mode = False,
|
995 |
-
# - 直接回答學生問題
|
996 |
-
# - 在你回答的開頭標註【一般學習精靈:{youtube_id} 】
|
997 |
-
# 如果學生問了一些問題你無法判斷,請告訴學生你無法判斷,並建議學生可以問其他問題
|
998 |
-
# 或者你可以反問學生一些問題,幫助學生更好的理解資料
|
999 |
-
# 如果學生的問題與資料文本無關,請告訴學生你無法回答超出範圍的問題
|
1000 |
-
# 最後只要是參考逐字稿資料,請在回答的最後標註【參考資料:(分):(秒)】
|
1001 |
-
# """
|
1002 |
-
|
1003 |
-
# 直接安排逐字稿資料 in instructions
|
1004 |
-
trascript_json = json.loads(trascript)
|
1005 |
-
# 移除 embed_url, screenshot_path
|
1006 |
-
for entry in trascript_json:
|
1007 |
-
entry.pop('embed_url', None)
|
1008 |
-
entry.pop('screenshot_path', None)
|
1009 |
-
trascript_text = json.dumps(trascript_json, ensure_ascii=False, indent=2)
|
1010 |
-
|
1011 |
-
instructions = f"""
|
1012 |
-
逐字稿資料:{trascript_text}
|
1013 |
-
-------------------------------------
|
1014 |
-
你是一個擅長資料分析跟影片教學的老師,user 為學生
|
1015 |
-
如果是影片類型,不用解釋逐字稿格式,直接回答學生問題
|
1016 |
-
socratic_mode = {socratic_mode}
|
1017 |
-
如果 socratic_mode = True,
|
1018 |
-
- 請用蘇格拉底式的提問方式,引導學生思考,並且給予學生一些提示
|
1019 |
-
- 不要直接給予答案,讓學生自己思考
|
1020 |
-
- 但可以給予一些提示跟引導,例如給予影片的時間軸,讓學生自己去找答案
|
1021 |
-
- 在你回答的開頭標註【蘇格拉底助教:{youtube_id} 】
|
1022 |
-
如果 socratic_mode = False,
|
1023 |
-
- 直接回答學生問題
|
1024 |
-
- 在你回答的開頭標註【一般學習精靈:{youtube_id} 】
|
1025 |
-
如果學生問了一些問題你無法判斷,請告訴學生你無法判斷,並建議學生可以問其他問題
|
1026 |
-
或者你可以反問學生一些問題,幫助學生更好的理解資料
|
1027 |
-
如果學生的問題與資料文本無關,請告訴學生你無法回答超出範圍的問題
|
1028 |
-
最後只要是參考逐字稿資料,請在回答的最後標註【參考資料:(分):(秒)】
|
1029 |
-
"""
|
1030 |
|
1031 |
-
|
1032 |
-
|
1033 |
-
|
1034 |
-
|
1035 |
-
|
1036 |
-
|
1037 |
-
|
1038 |
-
|
1039 |
-
|
1040 |
-
|
1041 |
-
|
1042 |
-
|
1043 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1044 |
|
1045 |
-
|
1046 |
-
|
1047 |
-
|
1048 |
-
|
1049 |
-
|
1050 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1051 |
|
1052 |
-
|
1053 |
-
|
1054 |
-
|
1055 |
-
|
1056 |
-
|
1057 |
-
|
1058 |
-
response_text = messages.data[0].content[0].text.value
|
1059 |
-
else:
|
1060 |
-
response_text = "學習精靈有點累,請稍後再試!"
|
1061 |
|
1062 |
-
|
1063 |
-
|
1064 |
-
|
1065 |
-
|
1066 |
-
|
1067 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1068 |
|
1069 |
# 返回聊天历史和空字符串清空输入框
|
1070 |
return "", chat_history, thread.id
|
1071 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1072 |
def poll_run_status(run_id, thread_id, timeout=600, poll_interval=5):
|
1073 |
"""
|
1074 |
Polls the status of a Run and handles different statuses appropriately.
|
@@ -1118,6 +1766,7 @@ def poll_run_status(run_id, thread_id, timeout=600, poll_interval=5):
|
|
1118 |
|
1119 |
return run.status
|
1120 |
|
|
|
1121 |
def update_slide(direction):
|
1122 |
global TRANSCRIPTS
|
1123 |
global CURRENT_INDEX
|
@@ -1145,7 +1794,37 @@ def prev_slide():
|
|
1145 |
def next_slide():
|
1146 |
return update_slide(1)
|
1147 |
|
1148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1149 |
|
1150 |
HEAD = """
|
1151 |
<meta charset="UTF-8">
|
@@ -1172,160 +1851,437 @@ HEAD = """
|
|
1172 |
});
|
1173 |
}
|
1174 |
</script>
|
1175 |
-
"""
|
1176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1177 |
|
1178 |
-
with gr.Blocks() as demo:
|
1179 |
-
with gr.Row():
|
1180 |
-
|
1181 |
-
|
1182 |
-
|
1183 |
-
|
1184 |
-
|
1185 |
-
|
1186 |
-
|
1187 |
-
|
1188 |
-
|
1189 |
-
|
1190 |
-
|
1191 |
-
|
1192 |
-
|
1193 |
-
|
1194 |
-
|
1195 |
-
|
1196 |
-
slide_image = gr.Image()
|
1197 |
-
slide_text = gr.Textbox()
|
1198 |
with gr.Row():
|
1199 |
-
|
1200 |
-
|
1201 |
-
|
1202 |
-
|
1203 |
-
|
1204 |
-
|
1205 |
-
|
1206 |
-
|
1207 |
-
|
1208 |
-
|
1209 |
-
with gr.Tab("
|
1210 |
-
|
1211 |
-
|
1212 |
-
|
1213 |
-
|
1214 |
-
|
1215 |
-
|
1216 |
-
|
1217 |
-
|
1218 |
-
|
1219 |
-
with gr.Tab("
|
1220 |
-
|
1221 |
-
|
1222 |
-
|
1223 |
-
|
1224 |
-
|
1225 |
-
|
1226 |
-
|
1227 |
-
|
1228 |
-
|
1229 |
-
|
1230 |
-
|
1231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1232 |
with gr.Tab("學習單"):
|
1233 |
-
|
1234 |
-
|
1235 |
-
|
1236 |
-
|
1237 |
-
|
1238 |
-
|
1239 |
-
|
1240 |
-
|
1241 |
-
|
1242 |
-
|
1243 |
-
|
1244 |
-
|
1245 |
-
|
1246 |
-
|
1247 |
-
|
1248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1249 |
|
1250 |
-
|
1251 |
-
|
1252 |
-
|
1253 |
-
|
1254 |
-
|
1255 |
-
|
1256 |
-
|
1257 |
-
|
1258 |
-
|
1259 |
-
|
1260 |
-
|
1261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1262 |
send_button.click(
|
1263 |
-
|
1264 |
-
inputs=[video_id, thread_id, df_string_output, msg, chatbot, socratic_mode_btn],
|
1265 |
outputs=[msg, chatbot, thread_id]
|
1266 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1267 |
# 连接按钮点击事件
|
|
|
|
|
|
|
1268 |
btn_1.click(
|
1269 |
-
|
1270 |
-
inputs=
|
1271 |
outputs=[msg, chatbot, thread_id]
|
1272 |
)
|
1273 |
btn_2.click(
|
1274 |
-
|
1275 |
-
inputs=
|
1276 |
outputs=[msg, chatbot, thread_id]
|
1277 |
)
|
1278 |
btn_3.click(
|
1279 |
-
|
1280 |
-
inputs=
|
1281 |
outputs=[msg, chatbot, thread_id]
|
1282 |
)
|
1283 |
|
1284 |
-
btn_create_question.click(
|
|
|
|
|
|
|
|
|
1285 |
|
1286 |
# file_upload.change(process_file, inputs=file_upload, outputs=df_string_output)
|
1287 |
file_upload.change(process_file, inputs=file_upload, outputs=[btn_1, btn_2, btn_3, df_summarise, df_string_output])
|
1288 |
|
1289 |
# 当输入 YouTube 链接时触发
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1290 |
youtube_link.change(
|
1291 |
process_youtube_link,
|
1292 |
-
inputs=youtube_link,
|
1293 |
-
outputs=
|
1294 |
-
|
1295 |
-
btn_1,
|
1296 |
-
btn_2,
|
1297 |
-
btn_3,
|
1298 |
-
df_string_output,
|
1299 |
-
df_summarise,
|
1300 |
-
mind_map,
|
1301 |
-
mind_map_html,
|
1302 |
-
transcript_html,
|
1303 |
-
simple_html_content,
|
1304 |
-
slide_image,
|
1305 |
-
slide_text
|
1306 |
-
]
|
1307 |
-
)
|
1308 |
|
1309 |
youtube_link_btn.click(
|
1310 |
process_youtube_link,
|
1311 |
-
inputs=youtube_link,
|
1312 |
-
outputs=
|
1313 |
-
|
1314 |
-
btn_1,
|
1315 |
-
btn_2,
|
1316 |
-
btn_3,
|
1317 |
-
df_string_output,
|
1318 |
-
df_summarise,
|
1319 |
-
mind_map,
|
1320 |
-
mind_map_html,
|
1321 |
-
transcript_html,
|
1322 |
-
simple_html_content,
|
1323 |
-
slide_image,
|
1324 |
-
slide_text
|
1325 |
-
]
|
1326 |
-
)
|
1327 |
|
1328 |
# 当输入网页链接时触发
|
1329 |
# web_link.change(process_web_link, inputs=web_link, outputs=[btn_1, btn_2, btn_3, df_summarise, df_string_output])
|
1330 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1331 |
demo.launch(allowed_paths=["videos"])
|
|
|
5 |
from docx import Document
|
6 |
import os
|
7 |
from openai import OpenAI
|
8 |
+
from groq import Groq
|
9 |
+
import uuid
|
10 |
+
from gtts import gTTS
|
11 |
+
import math
|
12 |
+
from pydub import AudioSegment
|
13 |
+
|
14 |
|
15 |
from youtube_transcript_api import YouTubeTranscriptApi
|
16 |
from youtube_transcript_api._errors import NoTranscriptFound
|
17 |
+
import yt_dlp
|
18 |
|
19 |
from moviepy.editor import VideoFileClip
|
20 |
from pytube import YouTube
|
21 |
import os
|
22 |
+
import io
|
23 |
+
import time
|
24 |
+
import json
|
25 |
+
from urllib.parse import urlparse, parse_qs
|
26 |
|
27 |
from google.cloud import storage
|
28 |
from google.oauth2 import service_account
|
|
|
31 |
from googleapiclient.http import MediaIoBaseDownload
|
32 |
from googleapiclient.http import MediaIoBaseUpload
|
33 |
|
34 |
+
from educational_material import EducationalMaterial
|
35 |
+
from storage_service import GoogleCloudStorage
|
36 |
+
|
37 |
+
import boto3
|
38 |
+
|
39 |
+
from chatbot import Chatbot
|
40 |
+
|
41 |
+
is_env_local = os.getenv("IS_ENV_LOCAL", "false") == "true"
|
42 |
+
print(f"is_env_local: {is_env_local}")
|
43 |
+
|
44 |
+
print("===gr__version__===")
|
45 |
+
print(gr.__version__)
|
46 |
+
|
47 |
+
|
48 |
+
if is_env_local:
|
49 |
+
with open("local_config.json") as f:
|
50 |
+
config = json.load(f)
|
51 |
+
PASSWORD = config["PASSWORD"]
|
52 |
+
GCS_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
|
53 |
+
DRIVE_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
|
54 |
+
OPEN_AI_KEY = config["OPEN_AI_KEY"]
|
55 |
+
GROQ_API_KEY = config["GROQ_API_KEY"]
|
56 |
+
JUTOR_CHAT_KEY = config["JUTOR_CHAT_KEY"]
|
57 |
+
AWS_ACCESS_KEY = config["AWS_ACCESS_KEY"]
|
58 |
+
AWS_SECRET_KEY = config["AWS_SECRET_KEY"]
|
59 |
+
AWS_REGION_NAME = config["AWS_REGION_NAME"]
|
60 |
+
OUTPUT_PATH = config["OUTPUT_PATH"]
|
61 |
+
else:
|
62 |
+
PASSWORD = os.getenv("PASSWORD")
|
63 |
+
GCS_KEY = os.getenv("GOOGLE_APPLICATION_CREDENTIALS_JSON")
|
64 |
+
DRIVE_KEY = os.getenv("GOOGLE_APPLICATION_CREDENTIALS_JSON")
|
65 |
+
OPEN_AI_KEY = os.getenv("OPEN_AI_KEY")
|
66 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
67 |
+
JUTOR_CHAT_KEY = os.getenv("JUTOR_CHAT_KEY")
|
68 |
+
AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY")
|
69 |
+
AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY")
|
70 |
+
AWS_REGION_NAME = 'us-west-2'
|
71 |
+
OUTPUT_PATH = 'videos'
|
72 |
|
|
|
73 |
TRANSCRIPTS = []
|
74 |
CURRENT_INDEX = 0
|
75 |
VIDEO_ID = ""
|
76 |
|
|
|
77 |
OPEN_AI_CLIENT = OpenAI(api_key=OPEN_AI_KEY)
|
78 |
+
GROQ_CLIENT = Groq(api_key=GROQ_API_KEY)
|
79 |
+
GCS_SERVICE = GoogleCloudStorage(GCS_KEY)
|
80 |
+
GCS_CLIENT = GCS_SERVICE.client
|
81 |
+
BEDROCK_CLIENT = boto3.client(
|
82 |
+
service_name="bedrock-runtime",
|
83 |
+
aws_access_key_id=AWS_ACCESS_KEY,
|
84 |
+
aws_secret_access_key=AWS_SECRET_KEY,
|
85 |
+
region_name=AWS_REGION_NAME,
|
86 |
+
)
|
87 |
+
|
88 |
+
# 驗證 password
|
89 |
+
def verify_password(password):
|
90 |
+
if password == PASSWORD:
|
91 |
+
return True
|
|
|
|
|
|
|
|
|
|
|
92 |
else:
|
93 |
+
raise gr.Error("密碼錯誤")
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
|
95 |
+
# ====gcs====
|
96 |
def gcs_check_file_exists(gcs_client, bucket_name, file_name):
|
97 |
"""
|
98 |
检查 GCS 存储桶中是否存在指定的文件
|
|
|
114 |
bucket = gcs_client.bucket(bucket_name)
|
115 |
blob = bucket.blob(destination_blob_name)
|
116 |
blob.upload_from_string(json_string)
|
117 |
+
print(f"JSON string uploaded to {destination_blob_name} in GCS.")
|
118 |
|
119 |
def download_blob_to_string(gcs_client, bucket_name, source_blob_name):
|
120 |
"""从 GCS 下载文件内容到字符串"""
|
|
|
175 |
blob.upload_from_string(file_content)
|
176 |
print(f"File {file_id} copied to GCS at {gcs_destination_path}.")
|
177 |
|
178 |
+
def delete_blob(gcs_client, bucket_name, blob_name):
|
179 |
+
"""删除指定的 GCS 对象"""
|
180 |
+
bucket = gcs_client.bucket(bucket_name)
|
181 |
+
blob = bucket.blob(blob_name)
|
182 |
+
blob.delete()
|
183 |
+
print(f"Blob {blob_name} deleted from GCS.")
|
184 |
+
|
185 |
# # ====drive====初始化
|
186 |
def init_drive_service():
|
187 |
credentials_json_string = DRIVE_KEY
|
|
|
306 |
|
307 |
print(f"文件已更新,文件ID: {updated_file['id']}")
|
308 |
|
309 |
+
# ---- Text file ----
|
310 |
+
def process_file(password, file):
|
311 |
+
verify_password(password)
|
312 |
+
|
313 |
# 读取文件
|
314 |
if file.name.endswith('.csv'):
|
315 |
df = pd.read_csv(file)
|
|
|
346 |
doc = Document(file)
|
347 |
return "\n".join([para.text for para in doc.paragraphs])
|
348 |
|
349 |
+
|
350 |
+
# ---- YouTube link ----
|
351 |
def format_seconds_to_time(seconds):
|
352 |
"""将秒数格式化为 时:分:秒 的形式"""
|
353 |
hours = int(seconds // 3600)
|
|
|
378 |
continue # 當前語言的字幕沒有找到,繼續嘗試下一個語言
|
379 |
return None # 所有嘗試都失敗,返回None
|
380 |
|
381 |
+
def generate_transcription(video_id):
|
382 |
+
youtube_url = f'https://www.youtube.com/watch?v={video_id}'
|
383 |
+
codec_name = "mp3"
|
384 |
+
outtmpl = f"{OUTPUT_PATH}/{video_id}.%(ext)s"
|
385 |
+
ydl_opts = {
|
386 |
+
'format': 'bestaudio/best',
|
387 |
+
'postprocessors': [{
|
388 |
+
'key': 'FFmpegExtractAudio',
|
389 |
+
'preferredcodec': codec_name,
|
390 |
+
'preferredquality': '192'
|
391 |
+
}],
|
392 |
+
'outtmpl': outtmpl,
|
393 |
+
}
|
394 |
+
|
395 |
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
396 |
+
ydl.download([youtube_url])
|
397 |
+
|
398 |
+
audio_path = f"{OUTPUT_PATH}/{video_id}.{codec_name}"
|
399 |
+
full_audio = AudioSegment.from_mp3(audio_path)
|
400 |
+
|
401 |
+
max_part_duration = 10 * 60 * 1000 # 10 minutes
|
402 |
+
full_duration = len(full_audio) # in milliseconds
|
403 |
+
parts = math.ceil(full_duration / max_part_duration)
|
404 |
+
print(f"parts: {parts}")
|
405 |
+
transcription = []
|
406 |
+
|
407 |
+
for i in range(parts):
|
408 |
+
print(f"== i: {i}==")
|
409 |
+
start_time = i * max_part_duration
|
410 |
+
end_time = min((i + 1) * max_part_duration, full_duration)
|
411 |
+
print(f"time: {start_time/1000} - {end_time/1000}")
|
412 |
+
chunk = full_audio[start_time:end_time]
|
413 |
+
chunk_path = f"{OUTPUT_PATH}/{video_id}_part_{i}.{codec_name}"
|
414 |
+
chunk.export(chunk_path, format=codec_name)
|
415 |
+
|
416 |
+
with open(chunk_path, "rb") as chunk_file:
|
417 |
+
response = OPEN_AI_CLIENT.audio.transcriptions.create(
|
418 |
+
model="whisper-1",
|
419 |
+
file=chunk_file,
|
420 |
+
response_format="verbose_json",
|
421 |
+
timestamp_granularities=["segment"],
|
422 |
+
prompt="Transcribe the following audio file. if chinese, please using 'language: zh-TW' ",
|
423 |
+
)
|
424 |
+
|
425 |
+
# Adjusting the timestamps for the chunk based on its position in the full audio
|
426 |
+
adjusted_segments = [{
|
427 |
+
'text': segment['text'],
|
428 |
+
'start': math.ceil(segment['start'] + start_time / 1000.0), # Converting milliseconds to seconds
|
429 |
+
'end': math.ceil(segment['end'] + start_time / 1000.0),
|
430 |
+
'duration': math.ceil(segment['end'] - segment['start'])
|
431 |
+
} for segment in response.segments]
|
432 |
+
|
433 |
+
transcription.extend(adjusted_segments)
|
434 |
+
|
435 |
+
# Remove temporary chunk files after processing
|
436 |
+
os.remove(chunk_path)
|
437 |
+
|
438 |
+
return transcription
|
439 |
+
|
440 |
def process_transcript_and_screenshots(video_id):
|
441 |
print("====process_transcript_and_screenshots====")
|
442 |
|
|
|
478 |
updated_transcript_text = json.dumps(transcript, ensure_ascii=False, indent=2)
|
479 |
update_file_on_drive(service, file_id, updated_transcript_text)
|
480 |
print("逐字稿已更新,包括截图链接")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
481 |
return transcript
|
482 |
|
483 |
def process_transcript_and_screenshots_on_gcs(video_id):
|
484 |
print("====process_transcript_and_screenshots_on_gcs====")
|
485 |
# GCS
|
486 |
+
gcs_client = GCS_CLIENT
|
487 |
bucket_name = 'video_ai_assistant'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
488 |
# 逐字稿文件名
|
489 |
transcript_file_name = f'{video_id}_transcript.json'
|
490 |
transcript_blob_name = f"{video_id}/{transcript_file_name}"
|
491 |
# 检查逐字稿是否存在
|
492 |
+
is_transcript_exists = GCS_SERVICE.check_file_exists(bucket_name, transcript_blob_name)
|
493 |
if not is_transcript_exists:
|
494 |
# 从YouTube获取逐字稿并上传
|
495 |
+
try:
|
496 |
+
transcript = get_transcript(video_id)
|
497 |
+
except:
|
498 |
+
# call open ai whisper
|
499 |
+
print("===call open ai whisper===")
|
500 |
+
transcript = generate_transcription(video_id)
|
501 |
+
|
502 |
if transcript:
|
503 |
print("成功獲取字幕")
|
504 |
else:
|
505 |
print("沒有找到字幕")
|
506 |
+
transcript = generate_transcription(video_id)
|
507 |
+
|
508 |
transcript_text = json.dumps(transcript, ensure_ascii=False, indent=2)
|
509 |
upload_file_to_gcs_with_json_string(gcs_client, bucket_name, transcript_blob_name, transcript_text)
|
510 |
else:
|
|
|
520 |
# get_mind_map(video_id, transcript_text, source)
|
521 |
# print("===確認其他衍生文件 end ===")
|
522 |
|
523 |
+
|
524 |
# 處理截圖
|
525 |
+
for entry in transcript:
|
526 |
+
if 'img_file_id' not in entry:
|
527 |
+
# 檢查 OUTPUT_PATH 是否存在 video_id.mp4
|
528 |
+
video_path = f'{OUTPUT_PATH}/{video_id}.mp4'
|
529 |
+
if not os.path.exists(video_path):
|
530 |
+
# try 5 times 如果都失敗就 raise
|
531 |
+
for i in range(5):
|
532 |
+
try:
|
533 |
+
download_youtube_video(video_id)
|
534 |
+
break
|
535 |
+
except Exception as e:
|
536 |
+
if i == 4:
|
537 |
+
raise gr.Error(f"下载视频失败: {str(e)}")
|
538 |
+
time.sleep(5)
|
539 |
+
# 截图
|
540 |
+
screenshot_path = screenshot_youtube_video(video_id, entry['start'])
|
541 |
+
screenshot_blob_name = f"{video_id}/{video_id}_{entry['start']}.jpg"
|
542 |
+
img_file_id = upload_img_and_get_public_url(gcs_client, bucket_name, screenshot_blob_name, screenshot_path)
|
543 |
+
entry['img_file_id'] = img_file_id
|
544 |
+
print(f"截图已上传到GCS: {img_file_id}")
|
545 |
|
546 |
# 更新逐字稿文件
|
547 |
+
print("===更新逐字稿文件===")
|
548 |
+
print(transcript)
|
549 |
+
print("===更新逐字稿文件===")
|
550 |
+
updated_transcript_text = json.dumps(transcript, ensure_ascii=False, indent=2)
|
551 |
+
upload_file_to_gcs_with_json_string(gcs_client, bucket_name, transcript_blob_name, updated_transcript_text)
|
552 |
+
print("逐字稿已更新,包括截图链接")
|
553 |
+
updated_transcript_json = json.loads(updated_transcript_text)
|
554 |
+
|
555 |
+
return updated_transcript_json
|
556 |
+
|
557 |
+
def process_youtube_link(password, link):
|
558 |
+
verify_password(password)
|
559 |
|
|
|
560 |
# 使用 YouTube API 获取逐字稿
|
561 |
# 假设您已经获取了 YouTube 视频的逐字稿并存储在变量 `transcript` 中
|
562 |
video_id = extract_youtube_id(link)
|
563 |
global VIDEO_ID
|
564 |
VIDEO_ID = video_id
|
|
|
565 |
|
566 |
try:
|
567 |
# transcript = process_transcript_and_screenshots(video_id)
|
|
|
579 |
start_time = format_seconds_to_time(entry['start'])
|
580 |
end_time = format_seconds_to_time(entry['start'] + entry['duration'])
|
581 |
embed_url = get_embedded_youtube_link(video_id, entry['start'])
|
582 |
+
img_file_id = entry['img_file_id']
|
583 |
+
# img_file_id =""
|
584 |
# 先取消 Google Drive 的图片
|
585 |
# screenshot_path = f"https://lh3.googleusercontent.com/d/{img_file_id}=s4000"
|
586 |
screenshot_path = img_file_id
|
|
|
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']
|
619 |
+
# first_image = "https://www.nameslook.com/names/dfsadf-nameslook.png"
|
620 |
first_text = formatted_transcript[0]['text']
|
621 |
mind_map_json = get_mind_map(video_id, formatted_simple_transcript, source)
|
622 |
mind_map = mind_map_json["mind_map"]
|
623 |
mind_map_html = get_mind_map_html(mind_map)
|
624 |
+
reading_passage_json = get_reading_passage(video_id, formatted_simple_transcript, source)
|
625 |
+
reading_passage = reading_passage_json["reading_passage"]
|
626 |
+
meta_data = get_meta_data(video_id)
|
627 |
+
subject = meta_data["subject"]
|
628 |
+
grade = meta_data["grade"]
|
629 |
|
630 |
# 确保返回与 UI 组件预期匹配的输出
|
631 |
return video_id, \
|
|
|
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, \
|
641 |
simple_html_content, \
|
642 |
first_image, \
|
643 |
+
first_text, \
|
644 |
+
reading_passage, \
|
645 |
+
subject, \
|
646 |
+
grade
|
647 |
|
648 |
def format_transcript_to_html(formatted_transcript):
|
649 |
html_content = ""
|
|
|
689 |
|
690 |
return screenshot_path
|
691 |
|
692 |
+
|
693 |
+
# ---- Web ----
|
694 |
def process_web_link(link):
|
695 |
# 抓取和解析网页内容
|
696 |
response = requests.get(link)
|
697 |
soup = BeautifulSoup(response.content, 'html.parser')
|
698 |
return soup.get_text()
|
699 |
|
700 |
+
|
701 |
+
# ---- LLM Generator ----
|
702 |
+
def get_reading_passage(video_id, df_string, source):
|
703 |
+
if source == "gcs":
|
704 |
+
print("===get_reading_passage on gcs===")
|
705 |
+
gcs_client = GCS_CLIENT
|
706 |
+
bucket_name = 'video_ai_assistant'
|
707 |
+
file_name = f'{video_id}_reading_passage.json'
|
708 |
+
blob_name = f"{video_id}/{file_name}"
|
709 |
+
# 检查 reading_passage 是否存在
|
710 |
+
is_file_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
|
711 |
+
if not is_file_exists:
|
712 |
+
reading_passage = generate_reading_passage(df_string)
|
713 |
+
reading_passage_json = {"reading_passage": str(reading_passage)}
|
714 |
+
reading_passage_text = json.dumps(reading_passage_json, ensure_ascii=False, indent=2)
|
715 |
+
upload_file_to_gcs_with_json_string(gcs_client, bucket_name, blob_name, reading_passage_text)
|
716 |
+
print("reading_passage已上传到GCS")
|
717 |
+
else:
|
718 |
+
# reading_passage已存在,下载内容
|
719 |
+
print("reading_passage已存在于GCS中")
|
720 |
+
reading_passage_text = download_blob_to_string(gcs_client, bucket_name, blob_name)
|
721 |
+
reading_passage_json = json.loads(reading_passage_text)
|
722 |
+
|
723 |
+
elif source == "drive":
|
724 |
+
print("===get_reading_passage on drive===")
|
725 |
+
service = init_drive_service()
|
726 |
+
parent_folder_id = '1GgI4YVs0KckwStVQkLa1NZ8IpaEMurkL'
|
727 |
+
folder_id = create_folder_if_not_exists(service, video_id, parent_folder_id)
|
728 |
+
file_name = f'{video_id}_reading_passage.json'
|
729 |
+
|
730 |
+
# 检查 reading_passage 是否存在
|
731 |
+
exists, file_id = check_file_exists(service, folder_id, file_name)
|
732 |
+
if not exists:
|
733 |
+
reading_passage = generate_reading_passage(df_string)
|
734 |
+
reading_passage_json = {"reading_passage": str(reading_passage)}
|
735 |
+
reading_passage_text = json.dumps(reading_passage_json, ensure_ascii=False, indent=2)
|
736 |
+
upload_content_directly(service, file_name, folder_id, reading_passage_text)
|
737 |
+
print("reading_passage已上��到Google Drive")
|
738 |
+
else:
|
739 |
+
# reading_passage已存在,下载内容
|
740 |
+
print("reading_passage已存在于Google Drive中")
|
741 |
+
reading_passage_text = download_file_as_string(service, file_id)
|
742 |
+
|
743 |
+
return reading_passage_json
|
744 |
+
|
745 |
+
def generate_reading_passage(df_string):
|
746 |
+
# 使用 OpenAI 生成基于上传数据的问题
|
747 |
+
sys_content = "你是一個擅長資料分析跟影片教學的老師,user 為學生,請精讀資料文本,自行判斷資料的種類,使用 zh-TW"
|
748 |
+
user_content = f"""
|
749 |
+
請根據 {df_string}
|
750 |
+
文本自行判斷資料的種類
|
751 |
+
幫我組合成 Reading Passage
|
752 |
+
並潤稿讓文句通順
|
753 |
+
請一定要使用繁體中文 zh-TW,並用台灣人的口語
|
754 |
+
產生的結果不要前後文解釋,也不要敘述這篇文章怎麼產生的
|
755 |
+
只需要專注提供 Reading Passage,字數在 500 字以內
|
756 |
+
"""
|
757 |
+
messages = [
|
758 |
+
{"role": "system", "content": sys_content},
|
759 |
+
{"role": "user", "content": user_content}
|
760 |
+
]
|
761 |
+
|
762 |
+
request_payload = {
|
763 |
+
"model": "gpt-4-1106-preview",
|
764 |
+
"messages": messages,
|
765 |
+
"max_tokens": 4000,
|
766 |
+
}
|
767 |
+
|
768 |
+
response = OPEN_AI_CLIENT.chat.completions.create(**request_payload)
|
769 |
+
reading_passage = response.choices[0].message.content.strip()
|
770 |
+
print("=====reading_passage=====")
|
771 |
+
print(reading_passage)
|
772 |
+
print("=====reading_passage=====")
|
773 |
+
|
774 |
+
return reading_passage
|
775 |
+
|
776 |
+
def text_to_speech(video_id, text):
|
777 |
+
tts = gTTS(text, lang='en')
|
778 |
+
filename = f'{video_id}_reading_passage.mp3'
|
779 |
+
tts.save(filename)
|
780 |
+
return filename
|
781 |
+
|
782 |
def get_mind_map(video_id, df_string, source):
|
783 |
if source == "gcs":
|
784 |
print("===get_mind_map on gcs===")
|
785 |
+
gcs_client = GCS_CLIENT
|
786 |
bucket_name = 'video_ai_assistant'
|
787 |
file_name = f'{video_id}_mind_map.json'
|
788 |
blob_name = f"{video_id}/{file_name}"
|
789 |
# 检查檔案是否存在
|
790 |
+
is_file_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
|
791 |
if not is_file_exists:
|
792 |
mind_map = generate_mind_map(df_string)
|
793 |
mind_map_json = {"mind_map": str(mind_map)}
|
|
|
865 |
def get_video_id_summary(video_id, df_string, source):
|
866 |
if source == "gcs":
|
867 |
print("===get_video_id_summary on gcs===")
|
868 |
+
gcs_client = GCS_CLIENT
|
869 |
bucket_name = 'video_ai_assistant'
|
870 |
file_name = f'{video_id}_summary.json'
|
871 |
summary_file_blob_name = f"{video_id}/{file_name}"
|
872 |
# 检查 summary_file 是否存在
|
873 |
+
is_summary_file_exists = GCS_SERVICE.check_file_exists(bucket_name, summary_file_blob_name)
|
874 |
if not is_summary_file_exists:
|
875 |
summary = generate_summarise(df_string)
|
876 |
summary_json = {"summary": str(summary)}
|
|
|
922 |
如果是資料類型,請提估欄位敘述、資料樣態與資料分析,告訴學生這張表的意義,以及可能的結論與對應方式
|
923 |
|
924 |
如果是影片類型,請提估影片內容,告訴學生這部影片的意義,
|
925 |
+
整體摘要在一百字以內
|
926 |
小範圍切出不同段落的相對應時間軸的重點摘要,最多不超過五段
|
927 |
注意不要遺漏任何一段時間軸的內容
|
928 |
格式為 【start - end】: 摘要
|
|
|
931 |
整體格式為:
|
932 |
🗂️ 1. 內容類型:?
|
933 |
📚 2. 整體摘要
|
934 |
+
🔖 3. 重點概念
|
935 |
+
🔑 4. 關鍵時刻
|
936 |
+
💡 5. 為什麼我們要學這個?
|
937 |
❓ 6. 延伸小問題
|
938 |
"""
|
939 |
|
|
|
963 |
|
964 |
return df_summarise
|
965 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
966 |
def get_questions(video_id, df_string, source="gcs"):
|
967 |
if source == "gcs":
|
968 |
# 去 gcs 確認是有有 video_id_questions.json
|
969 |
print("===get_questions on gcs===")
|
970 |
+
gcs_client = GCS_CLIENT
|
971 |
bucket_name = 'video_ai_assistant'
|
972 |
file_name = f'{video_id}_questions.json'
|
973 |
blob_name = f"{video_id}/{file_name}"
|
974 |
# 检查檔案是否存在
|
975 |
+
is_questions_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
|
976 |
if not is_questions_exists:
|
977 |
questions = generate_questions(df_string)
|
978 |
questions_text = json.dumps(questions, ensure_ascii=False, indent=2)
|
|
|
1015 |
print("=====get_questions=====")
|
1016 |
return q1, q2, q3
|
1017 |
|
1018 |
+
def generate_questions(df_string):
|
1019 |
+
# 使用 OpenAI 生成基于上传数据的问题
|
1020 |
+
|
1021 |
+
sys_content = "你是一個擅長資料分析跟影片教學的老師,user 為學生,請精讀資料文本,自行判斷資料的種類,並用既有資料為本質猜測用戶可能會問的問題,使用 zh-TW"
|
1022 |
+
user_content = f"請根據 {df_string} 生成三個問題,並用 JSON 格式返回 questions:[q1的敘述text, q2的敘述text, q3的敘述text]"
|
1023 |
+
messages = [
|
1024 |
+
{"role": "system", "content": sys_content},
|
1025 |
+
{"role": "user", "content": user_content}
|
1026 |
+
]
|
1027 |
+
response_format = { "type": "json_object" }
|
1028 |
+
|
1029 |
+
print("=====messages=====")
|
1030 |
+
print(messages)
|
1031 |
+
print("=====messages=====")
|
1032 |
+
|
1033 |
+
|
1034 |
+
request_payload = {
|
1035 |
+
"model": "gpt-4-1106-preview",
|
1036 |
+
"messages": messages,
|
1037 |
+
"max_tokens": 4000,
|
1038 |
+
"response_format": response_format
|
1039 |
+
}
|
1040 |
+
|
1041 |
+
response = OPEN_AI_CLIENT.chat.completions.create(**request_payload)
|
1042 |
+
questions = json.loads(response.choices[0].message.content)["questions"]
|
1043 |
+
print("=====json_response=====")
|
1044 |
+
print(questions)
|
1045 |
+
print("=====json_response=====")
|
1046 |
+
|
1047 |
+
return questions
|
1048 |
+
|
1049 |
+
def change_questions(password, df_string):
|
1050 |
+
verify_password(password)
|
1051 |
+
|
1052 |
questions = generate_questions(df_string)
|
1053 |
q1 = questions[0] if len(questions) > 0 else ""
|
1054 |
q2 = questions[1] if len(questions) > 1 else ""
|
|
|
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 的總內容,也就是大約 3~5段的重點(例如五~十分鐘的影片就一段大約1~2分鐘,最多三分鐘,但如果是超過十分鐘的影片,那一小段大約 2~3分鐘,以此類推)
|
1116 |
+
3. 注意不要遺漏任何一段時間軸的內容 從零秒開始
|
1117 |
+
4. 如果頭尾的情節不是重點,就併入到附近的段落,特別是打招呼或是介紹人物就是不重要的情節
|
1118 |
+
5. transcript 逐字稿的集合(要有合理的標點符號),要完整跟原來的一樣,不要省略
|
1119 |
+
以這種方式分析整個文本,從零秒開始分析,直到結束。這很重要
|
1120 |
+
|
1121 |
+
並用 JSON 格式返回 key_moments:[{{
|
1122 |
+
"start": "00:00",
|
1123 |
+
"end": "00:00",
|
1124 |
+
"text": "逐字稿的重點摘要",
|
1125 |
+
"transcript": "逐字稿的集合(要有合理的標點符號),要完整跟原來的一樣,不要省略",
|
1126 |
+
"images": 截圖的連結們 list
|
1127 |
+
}}]
|
1128 |
+
"""
|
1129 |
+
messages = [
|
1130 |
+
{"role": "system", "content": sys_content},
|
1131 |
+
{"role": "user", "content": user_content}
|
1132 |
+
]
|
1133 |
+
response_format = { "type": "json_object" }
|
1134 |
|
1135 |
+
request_payload = {
|
1136 |
+
"model": "gpt-4-1106-preview",
|
1137 |
+
"messages": messages,
|
1138 |
+
"max_tokens": 4096,
|
1139 |
+
"response_format": response_format
|
1140 |
+
}
|
1141 |
|
1142 |
+
try:
|
1143 |
+
response = OPEN_AI_CLIENT.chat.completions.create(**request_payload)
|
1144 |
+
key_moments = json.loads(response.choices[0].message.content)["key_moments"]
|
1145 |
+
except Exception as e:
|
1146 |
+
error_msg = f" {video_id} 關鍵時刻錯誤: {str(e)}"
|
1147 |
+
print("===generate_key_moments error===")
|
1148 |
+
print(error_msg)
|
1149 |
+
print("===generate_key_moments error===")
|
1150 |
+
raise Exception(error_msg)
|
1151 |
+
|
1152 |
+
print("=====key_moments=====")
|
1153 |
+
print(key_moments)
|
1154 |
+
print("=====key_moments=====")
|
1155 |
+
image_links = {entry['start_time']: entry['screenshot_path'] for entry in formatted_transcript}
|
1156 |
+
for moment in key_moments:
|
1157 |
+
start_time = moment['start']
|
1158 |
+
end_time = moment['end']
|
1159 |
+
moment_images = [image_links[time] for time in image_links if start_time <= time <= end_time]
|
1160 |
+
moment['images'] = moment_images
|
1161 |
+
|
1162 |
+
return key_moments
|
1163 |
+
|
1164 |
+
def get_key_moments_html(key_moments):
|
1165 |
+
css = """
|
1166 |
+
<style>
|
1167 |
+
#gallery-main {
|
1168 |
+
display: flex;
|
1169 |
+
align-items: center;
|
1170 |
+
margin-bottom: 20px;
|
1171 |
+
}
|
1172 |
|
1173 |
+
#gallery {
|
1174 |
+
position: relative;
|
1175 |
+
width: 50%;
|
1176 |
+
flex: 1;
|
1177 |
+
}
|
|
|
|
|
1178 |
|
1179 |
+
#text-content {
|
1180 |
+
flex: 2;
|
1181 |
+
margin-left: 20px;
|
1182 |
+
}
|
1183 |
|
|
|
|
|
1184 |
|
1185 |
+
#gallery #gallery-container{
|
1186 |
+
position: relative;
|
1187 |
+
width: 100%;
|
1188 |
+
height: 0px;
|
1189 |
+
padding-bottom: 56.7%; /* 16/9 ratio */
|
1190 |
+
background-color: blue;
|
1191 |
+
}
|
1192 |
+
#gallery #gallery-container #gallery-content{
|
1193 |
+
position: absolute;
|
1194 |
+
top: 0px;
|
1195 |
+
right: 0px;
|
1196 |
+
bottom: 0px;
|
1197 |
+
left: 0px;
|
1198 |
+
height: 100%;
|
1199 |
+
display: flex;
|
1200 |
+
scroll-snap-type: x mandatory;
|
1201 |
+
overflow-x: scroll;
|
1202 |
+
scroll-behavior: smooth;
|
1203 |
+
}
|
1204 |
+
#gallery #gallery-container #gallery-content .gallery__item{
|
1205 |
+
width: 100%;
|
1206 |
+
height: 100%;
|
1207 |
+
flex-shrink: 0;
|
1208 |
+
scroll-snap-align: start;
|
1209 |
+
scroll-snap-stop: always;
|
1210 |
+
position: relative;
|
1211 |
+
}
|
1212 |
+
#gallery #gallery-container #gallery-content .gallery__item img{
|
1213 |
+
display: block;
|
1214 |
+
width: 100%;
|
1215 |
+
height: 100%;
|
1216 |
+
object-fit: contain;
|
1217 |
+
background-color: white;
|
1218 |
+
}
|
1219 |
|
1220 |
+
.click-zone{
|
1221 |
+
position: absolute;
|
1222 |
+
width: 20%;
|
1223 |
+
height: 100%;
|
1224 |
+
z-index: 3;
|
1225 |
+
}
|
1226 |
+
.click-zone.click-zone-prev{
|
1227 |
+
left: 0px;
|
1228 |
+
}
|
1229 |
+
.click-zone.click-zone-next{
|
1230 |
+
right: 0px;
|
1231 |
+
}
|
1232 |
+
#gallery:not(:hover) .arrow{
|
1233 |
+
opacity: 0.8;
|
1234 |
+
}
|
1235 |
+
.arrow{
|
1236 |
+
text-align: center;
|
1237 |
+
z-index: 3;
|
1238 |
+
position: absolute;
|
1239 |
+
display: block;
|
1240 |
+
width: 25px;
|
1241 |
+
height: 25px;
|
1242 |
+
line-height: 25px;
|
1243 |
+
background-color: black;
|
1244 |
+
border-radius: 50%;
|
1245 |
+
text-decoration: none;
|
1246 |
+
color: black;
|
1247 |
+
opacity: 0.8;
|
1248 |
+
transition: opacity 200ms ease;
|
1249 |
+
}
|
1250 |
+
.arrow:hover{
|
1251 |
+
opacity: 1;
|
1252 |
+
}
|
1253 |
+
.arrow span{
|
1254 |
+
position: relative;
|
1255 |
+
top: 2px;
|
1256 |
+
}
|
1257 |
+
.arrow.arrow-prev{
|
1258 |
+
top: 50%;
|
1259 |
+
left: 5px;
|
1260 |
+
}
|
1261 |
+
.arrow.arrow-next{
|
1262 |
+
top: 50%;
|
1263 |
+
right: 5px;
|
1264 |
+
}
|
1265 |
+
.arrow.arrow-disabled{
|
1266 |
+
opacity:0.8;
|
1267 |
+
}
|
1268 |
|
1269 |
+
#text-content {
|
1270 |
+
padding: 0px 36px;
|
1271 |
+
}
|
1272 |
+
#text-content p {
|
1273 |
+
margin-top: 10px;
|
1274 |
+
}
|
1275 |
+
|
1276 |
+
body{
|
1277 |
+
font-family: sans-serif;
|
1278 |
+
margin: 0px;
|
1279 |
+
padding: 0px;
|
1280 |
+
}
|
1281 |
+
main{
|
1282 |
+
padding: 0px;
|
1283 |
+
margin: 0px;
|
1284 |
+
max-width: 900px;
|
1285 |
+
margin: auto;
|
1286 |
+
}
|
1287 |
+
.hidden{
|
1288 |
+
border: 0;
|
1289 |
+
clip: rect(0 0 0 0);
|
1290 |
+
height: 1px;
|
1291 |
+
margin: -1px;
|
1292 |
+
overflow: hidden;
|
1293 |
+
padding: 0;
|
1294 |
+
position: absolute;
|
1295 |
+
width: 1px;
|
1296 |
+
}
|
1297 |
+
|
1298 |
+
@media (max-width: 768px) {
|
1299 |
+
#gallery-main {
|
1300 |
+
flex-direction: column; /* 在小屏幕上堆叠元素 */
|
1301 |
+
}
|
1302 |
+
|
1303 |
+
#gallery {
|
1304 |
+
width: 100%; /* 让画廊占满整个容器宽度 */
|
1305 |
+
}
|
1306 |
+
|
1307 |
+
#text-content {
|
1308 |
+
margin-left: 0; /* 移除左边距,让文本内容占满宽度 */
|
1309 |
+
margin-top: 20px; /* 为文本内容添加顶部间距 */
|
1310 |
+
}
|
1311 |
+
|
1312 |
+
#gallery #gallery-container {
|
1313 |
+
height: 350px; /* 或者你可以设置一个固定的高度,而不是用 padding-bottom */
|
1314 |
+
padding-bottom: 0; /* 移除底部填充 */
|
1315 |
+
}
|
1316 |
+
}
|
1317 |
+
</style>
|
1318 |
+
"""
|
1319 |
+
|
1320 |
+
key_moments_html = css
|
1321 |
+
|
1322 |
+
for i, moment in enumerate(key_moments):
|
1323 |
+
images = moment['images']
|
1324 |
+
image_elements = ""
|
1325 |
|
1326 |
+
for j, image in enumerate(images):
|
1327 |
+
current_id = f"img_{i}_{j}"
|
1328 |
+
prev_id = f"img_{i}_{j-1}" if j-1 >= 0 else f"img_{i}_{len(images)-1}"
|
1329 |
+
next_id = f"img_{i}_{j+1}" if j+1 < len(images) else f"img_{i}_0"
|
1330 |
+
|
1331 |
+
image_elements += f"""
|
1332 |
+
<div id="{current_id}" class="gallery__item">
|
1333 |
+
<a href="#{prev_id}" class="click-zone click-zone-prev">
|
1334 |
+
<div class="arrow arrow-disabled arrow-prev"> < </div>
|
1335 |
+
</a>
|
1336 |
+
<a href="#{next_id}" class="click-zone click-zone-next">
|
1337 |
+
<div class="arrow arrow-next"> > </div>
|
1338 |
+
</a>
|
1339 |
+
<img src="{image}">
|
1340 |
+
</div>
|
1341 |
+
"""
|
1342 |
+
|
1343 |
+
gallery_content = f"""
|
1344 |
+
<div id="gallery-content">
|
1345 |
+
{image_elements}
|
1346 |
+
</div>
|
1347 |
+
"""
|
1348 |
+
|
1349 |
+
key_moments_html += f"""
|
1350 |
+
<div class="gallery-container" id="gallery-main">
|
1351 |
+
<div id="gallery"><!-- gallery start -->
|
1352 |
+
<div id="gallery-container">
|
1353 |
+
{gallery_content}
|
1354 |
+
</div>
|
1355 |
+
</div>
|
1356 |
+
<div id="text-content">
|
1357 |
+
<h3>{moment['start']} - {moment['end']}</h3>
|
1358 |
+
<p><strong>摘要: {moment['text']} </strong></p>
|
1359 |
+
<p>內容: {moment['transcript']}</p>
|
1360 |
+
</div>
|
1361 |
+
</div>
|
1362 |
+
"""
|
1363 |
+
|
1364 |
+
return key_moments_html
|
1365 |
+
|
1366 |
+
# ---- LLM CRUD ----
|
1367 |
+
def enable_edit_mode():
|
1368 |
+
return gr.update(interactive=True)
|
1369 |
+
|
1370 |
+
def delete_LLM_content(video_id, kind):
|
1371 |
+
print(f"===delete_{kind}===")
|
1372 |
+
gcs_client = GCS_CLIENT
|
1373 |
+
bucket_name = 'video_ai_assistant'
|
1374 |
+
file_name = f'{video_id}_{kind}.json'
|
1375 |
+
blob_name = f"{video_id}/{file_name}"
|
1376 |
+
# 检查 reading_passage 是否存在
|
1377 |
+
is_file_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
|
1378 |
+
if is_file_exists:
|
1379 |
+
delete_blob(gcs_client, bucket_name, blob_name)
|
1380 |
+
print("reading_passage已从GCS中删除")
|
1381 |
+
return gr.update(value="", interactive=False)
|
1382 |
+
|
1383 |
+
def update_LLM_content(video_id, new_content, kind):
|
1384 |
+
print(f"===upfdate kind on gcs===")
|
1385 |
+
gcs_client = GCS_CLIENT
|
1386 |
+
bucket_name = 'video_ai_assistant'
|
1387 |
+
file_name = f'{video_id}_{kind}.json'
|
1388 |
+
blob_name = f"{video_id}/{file_name}"
|
1389 |
+
|
1390 |
+
if kind == "reading_passage":
|
1391 |
+
reading_passage_json = {"reading_passage": str(new_content)}
|
1392 |
+
reading_passage_text = json.dumps(reading_passage_json, ensure_ascii=False, indent=2)
|
1393 |
+
upload_file_to_gcs_with_json_string(gcs_client, bucket_name, blob_name, reading_passage_text)
|
1394 |
+
elif kind == "summary":
|
1395 |
+
summary_json = {"summary": str(new_content)}
|
1396 |
+
summary_text = json.dumps(summary_json, ensure_ascii=False, indent=2)
|
1397 |
+
upload_file_to_gcs_with_json_string(gcs_client, bucket_name, blob_name, summary_text)
|
1398 |
+
elif kind == "mind_map":
|
1399 |
+
mind_map_json = {"mind_map": str(new_content)}
|
1400 |
+
mind_map_text = json.dumps(mind_map_json, ensure_ascii=False, indent=2)
|
1401 |
+
upload_file_to_gcs_with_json_string(gcs_client, bucket_name, blob_name, mind_map_text)
|
1402 |
+
|
1403 |
+
print(f"{kind} 已更新到GCS")
|
1404 |
+
return gr.update(value=new_content, interactive=False)
|
1405 |
+
|
1406 |
+
def create_LLM_content(video_id, df_string, kind):
|
1407 |
+
print(f"===create_{kind}===")
|
1408 |
+
if kind == "reading_passage":
|
1409 |
+
content = generate_reading_passage(df_string)
|
1410 |
+
elif kind == "summary":
|
1411 |
+
content = generate_summarise(df_string)
|
1412 |
+
elif kind == "mind_map":
|
1413 |
+
content = generate_mind_map(df_string)
|
1414 |
+
|
1415 |
+
update_LLM_content(video_id, content, kind)
|
1416 |
+
return gr.update(value=content, interactive=False)
|
1417 |
+
|
1418 |
+
|
1419 |
+
# AI 生成教學素材
|
1420 |
+
def get_meta_data(video_id, source="gcs"):
|
1421 |
+
if source == "gcs":
|
1422 |
+
print("===get_meta_data on gcs===")
|
1423 |
+
gcs_client = GCS_CLIENT
|
1424 |
+
bucket_name = 'video_ai_assistant'
|
1425 |
+
file_name = f'{video_id}_meta_data.json'
|
1426 |
+
blob_name = f"{video_id}/{file_name}"
|
1427 |
+
# 检查檔案是否存在
|
1428 |
+
is_file_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
|
1429 |
+
if not is_file_exists:
|
1430 |
+
meta_data_json = {
|
1431 |
+
"subject": "",
|
1432 |
+
"grade": "",
|
1433 |
+
}
|
1434 |
+
print("meta_data empty return")
|
1435 |
+
else:
|
1436 |
+
# meta_data已存在,下载内容
|
1437 |
+
print("meta_data已存在于GCS中")
|
1438 |
+
meta_data_text = download_blob_to_string(gcs_client, bucket_name, blob_name)
|
1439 |
+
meta_data_json = json.loads(meta_data_text)
|
1440 |
+
|
1441 |
+
# meta_data_json grade 數字轉換成文字
|
1442 |
+
grade = meta_data_json["grade"]
|
1443 |
+
case = {
|
1444 |
+
1: "一年級",
|
1445 |
+
2: "二年級",
|
1446 |
+
3: "三年級",
|
1447 |
+
4: "四年級",
|
1448 |
+
5: "五年級",
|
1449 |
+
6: "六年級",
|
1450 |
+
7: "七年級",
|
1451 |
+
8: "八年級",
|
1452 |
+
9: "九年級",
|
1453 |
+
10: "十年級",
|
1454 |
+
11: "十一年級",
|
1455 |
+
12: "十二年級",
|
1456 |
+
}
|
1457 |
+
grade_text = case.get(grade, "")
|
1458 |
+
meta_data_json["grade"] = grade_text
|
1459 |
+
|
1460 |
+
return meta_data_json
|
1461 |
+
|
1462 |
+
def get_ai_content(password, video_id, df_string, topic, grade, level, specific_feature, content_type, source="gcs"):
|
1463 |
+
verify_password(password)
|
1464 |
+
if source == "gcs":
|
1465 |
+
print("===get_ai_content on gcs===")
|
1466 |
+
gcs_client = GCS_CLIENT
|
1467 |
+
bucket_name = 'video_ai_assistant'
|
1468 |
+
file_name = f'{video_id}_ai_content_list.json'
|
1469 |
+
blob_name = f"{video_id}/{file_name}"
|
1470 |
+
# 检查檔案是否存在
|
1471 |
+
is_file_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
|
1472 |
+
if not is_file_exists:
|
1473 |
+
# 先建立一個 ai_content_list.json
|
1474 |
+
ai_content_list = []
|
1475 |
+
ai_content_text = json.dumps(ai_content_list, ensure_ascii=False, indent=2)
|
1476 |
+
upload_file_to_gcs_with_json_string(gcs_client, bucket_name, blob_name, ai_content_text)
|
1477 |
+
print("ai_content_list [] 已上傳到GCS")
|
1478 |
+
|
1479 |
+
# 此時 ai_content_list 已存在
|
1480 |
+
ai_content_list_string = download_blob_to_string(gcs_client, bucket_name, blob_name)
|
1481 |
+
ai_content_list = json.loads(ai_content_list_string)
|
1482 |
+
# by key 找到 ai_content (topic, grade, level, specific_feature, content_type)
|
1483 |
+
target_kvs = {
|
1484 |
+
"video_id": video_id,
|
1485 |
+
"level": level,
|
1486 |
+
"specific_feature": specific_feature,
|
1487 |
+
"content_type": content_type
|
1488 |
+
}
|
1489 |
+
ai_content_json = [
|
1490 |
+
item for item in ai_content_list
|
1491 |
+
if all(item[k] == v for k, v in target_kvs.items())
|
1492 |
+
]
|
1493 |
+
|
1494 |
+
if len(ai_content_json) == 0:
|
1495 |
+
ai_content, prompt = generate_ai_content(password, df_string, topic, grade, level, specific_feature, content_type)
|
1496 |
+
ai_content_json = {
|
1497 |
+
"video_id": video_id,
|
1498 |
+
"content": str(ai_content),
|
1499 |
+
"prompt": prompt,
|
1500 |
+
"level": level,
|
1501 |
+
"specific_feature": specific_feature,
|
1502 |
+
"content_type": content_type
|
1503 |
+
}
|
1504 |
+
|
1505 |
+
ai_content_list.append(ai_content_json)
|
1506 |
+
ai_content_text = json.dumps(ai_content_list, ensure_ascii=False, indent=2)
|
1507 |
+
upload_file_to_gcs_with_json_string(gcs_client, bucket_name, blob_name, ai_content_text)
|
1508 |
+
print("ai_content已上傳到GCS")
|
1509 |
+
else:
|
1510 |
+
ai_content_json = ai_content_json[-1]
|
1511 |
+
ai_content = ai_content_json["content"]
|
1512 |
+
prompt = ai_content_json["prompt"]
|
1513 |
+
|
1514 |
+
return ai_content, ai_content, prompt, prompt
|
1515 |
+
|
1516 |
+
def generate_ai_content(password, df_string, topic, grade, level, specific_feature, content_type):
|
1517 |
+
verify_password(password)
|
1518 |
+
material = EducationalMaterial(df_string, topic, grade, level, specific_feature, content_type)
|
1519 |
+
prompt = material.generate_content_prompt()
|
1520 |
+
user_content = material.build_user_content()
|
1521 |
+
messages = material.build_messages(user_content)
|
1522 |
+
ai_model_name = "gpt-4-1106-preview"
|
1523 |
request_payload = {
|
1524 |
+
"model": ai_model_name,
|
1525 |
"messages": messages,
|
1526 |
+
"max_tokens": 4000 # 举例,实际上您可能需要更详细的配置
|
1527 |
}
|
1528 |
+
ai_content = material.send_ai_request(OPEN_AI_CLIENT, request_payload)
|
1529 |
+
return ai_content, prompt
|
1530 |
+
|
1531 |
+
def generate_exam_fine_tune_result(password, exam_result_prompt , df_string_output, exam_result, exam_result_fine_tune_prompt):
|
1532 |
+
verify_password(password)
|
1533 |
+
material = EducationalMaterial(df_string_output, "", "", "", "", "")
|
1534 |
+
user_content = material.build_fine_tune_user_content(exam_result_prompt, exam_result, exam_result_fine_tune_prompt)
|
1535 |
+
messages = material.build_messages(user_content)
|
1536 |
+
ai_model_name = "gpt-4-1106-preview"
|
1537 |
+
request_payload = {
|
1538 |
+
"model": ai_model_name,
|
1539 |
+
"messages": messages,
|
1540 |
+
"max_tokens": 4000 # 举例,实际上您可能需要更详细的配置
|
1541 |
+
}
|
1542 |
+
ai_content = material.send_ai_request(OPEN_AI_CLIENT, request_payload)
|
1543 |
+
return ai_content
|
1544 |
+
|
1545 |
+
def return_original_exam_result(exam_result_original):
|
1546 |
+
return exam_result_original
|
1547 |
+
|
1548 |
+
def create_word(content):
|
1549 |
+
unique_filename = str(uuid.uuid4())
|
1550 |
+
word_file_path = f"/tmp/{unique_filename}.docx"
|
1551 |
+
doc = Document()
|
1552 |
+
doc.add_paragraph(content)
|
1553 |
+
doc.save(word_file_path)
|
1554 |
+
return word_file_path
|
1555 |
+
|
1556 |
+
def download_exam_result(content):
|
1557 |
+
word_path = create_word(content)
|
1558 |
+
return word_path
|
1559 |
+
|
1560 |
+
# ---- Chatbot ----
|
1561 |
+
def chat_with_ai(ai_name, password, video_id, trascript, user_message, chat_history, content_subject, content_grade, socratic_mode=False):
|
1562 |
+
verify_password(password)
|
1563 |
+
|
1564 |
+
if chat_history is not None and len(chat_history) > 10:
|
1565 |
+
error_msg = "此次對話超過上限"
|
1566 |
+
raise gr.Error(error_msg)
|
1567 |
|
1568 |
+
if ai_name == "jutor":
|
1569 |
+
ai_client = ""
|
1570 |
+
elif ai_name == "claude3":
|
1571 |
+
ai_client = BEDROCK_CLIENT
|
1572 |
+
elif ai_name == "groq":
|
1573 |
+
ai_client = GROQ_CLIENT
|
1574 |
+
|
1575 |
+
chatbot_config = {
|
1576 |
+
"video_id": video_id,
|
1577 |
+
"trascript": trascript,
|
1578 |
+
"content_subject": content_subject,
|
1579 |
+
"content_grade": content_grade,
|
1580 |
+
"jutor_chat_key": JUTOR_CHAT_KEY,
|
1581 |
+
"ai_name": ai_name,
|
1582 |
+
"ai_client": ai_client
|
1583 |
+
}
|
1584 |
+
chatbot = Chatbot(chatbot_config)
|
1585 |
+
response_completion = chatbot.chat(user_message, chat_history, socratic_mode, ai_name)
|
1586 |
|
1587 |
+
try:
|
1588 |
+
# 更新聊天历史
|
1589 |
+
new_chat_history = (user_message, response_completion)
|
1590 |
+
if chat_history is None:
|
1591 |
+
chat_history = [new_chat_history]
|
1592 |
+
else:
|
1593 |
+
chat_history.append(new_chat_history)
|
1594 |
+
|
1595 |
+
# 返回聊天历史和空字符串清空输入框
|
1596 |
+
return "", chat_history
|
1597 |
+
except Exception as e:
|
1598 |
+
# 处理错误情况
|
1599 |
+
print(f"Error: {e}")
|
1600 |
+
return "请求失败,请稍后再试!", chat_history
|
1601 |
+
|
1602 |
+
def chat_with_opan_ai_assistant(password, youtube_id, thread_id, trascript, user_message, chat_history, content_subject, content_grade, socratic_mode=False):
|
1603 |
+
verify_password(password)
|
1604 |
|
|
|
1605 |
# 先計算 user_message 是否超過 500 個字
|
1606 |
if len(user_message) > 1500:
|
1607 |
error_msg = "你的訊息太長了,請縮短訊息長度至五百字以內"
|
1608 |
raise gr.Error(error_msg)
|
1609 |
|
1610 |
+
# 如果 chat_history 超過 10 則訊息,直接 return "對話超過上限"
|
1611 |
+
if chat_history is not None and len(chat_history) > 10:
|
1612 |
+
error_msg = "此次對話超過上限"
|
1613 |
+
raise gr.Error(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1614 |
|
1615 |
+
try:
|
1616 |
+
assistant_id = "asst_kmvZLNkDUYaNkMNtZEAYxyPq"
|
1617 |
+
client = OPEN_AI_CLIENT
|
1618 |
+
# 直接安排逐字稿資料 in instructions
|
1619 |
+
trascript_json = json.loads(trascript)
|
1620 |
+
# 移除 embed_url, screenshot_path
|
1621 |
+
for entry in trascript_json:
|
1622 |
+
entry.pop('embed_url', None)
|
1623 |
+
entry.pop('screenshot_path', None)
|
1624 |
+
trascript_text = json.dumps(trascript_json, ensure_ascii=False, indent=2)
|
1625 |
+
|
1626 |
+
instructions = f"""
|
1627 |
+
科目:{content_subject}
|
1628 |
+
年級:{content_grade}
|
1629 |
+
逐字稿資料:{trascript_text}
|
1630 |
+
-------------------------------------
|
1631 |
+
你是一個專業的{content_subject}老師, user 為{content_grade}的學生
|
1632 |
+
socratic_mode = {socratic_mode}
|
1633 |
+
if socratic_mode is True,
|
1634 |
+
- 請用蘇格拉底式的提問方式,引導學生思考,並且給予學生一些提示
|
1635 |
+
- 一次只問一個問題,字數在100字以內
|
1636 |
+
- 不要直接給予答案,讓學生自己思考
|
1637 |
+
- 但可以給予一些提示跟引導,例如給予影片的時間軸,讓學生自己去找答案
|
1638 |
+
|
1639 |
+
if socratic_mode is False,
|
1640 |
+
- 直接回答學生問題,字數在100字以內
|
1641 |
+
|
1642 |
+
rule:
|
1643 |
+
- 請一定要用繁體中文回答 zh-TW,並用台灣人的口語表達,回答時不用特別說明這是台灣人的語氣,也不用說這是「台語的說法」
|
1644 |
+
- 不用提到「逐字稿」這個詞,用「內容」代替
|
1645 |
+
- 如果學生問了一些問題你無法判斷,請告訴學生你無法判斷,並建議學生可以問其他問題
|
1646 |
+
- 或者你可以反問學生一些問題,幫助學生更好的理解資料,字數在100字以內
|
1647 |
+
- 如果學生的問題與資料文本無關,請告訴學生你「無法回答超出影片範圍的問題」,並告訴他可以怎麼問什麼樣的問題(一個就好)
|
1648 |
+
- 只要是參考逐字稿資料,請在回答的最後標註【參考資料:(分):(秒)】
|
1649 |
+
- 回答範圍一定要在逐字稿資料內,不要引用其他資料,請嚴格執行
|
1650 |
+
- 並在重複問句後給予學生鼓勵,讓學生有學習的動力
|
1651 |
+
- 請用 {content_grade} 的學生能懂的方式回答
|
1652 |
+
"""
|
1653 |
|
1654 |
+
# 创建线程
|
1655 |
+
if not thread_id:
|
1656 |
+
thread = client.beta.threads.create()
|
1657 |
+
thread_id = thread.id
|
1658 |
+
else:
|
1659 |
+
thread = client.beta.threads.retrieve(thread_id)
|
1660 |
+
|
1661 |
+
# 向线程添加用户的消息
|
1662 |
+
client.beta.threads.messages.create(
|
1663 |
+
thread_id=thread.id,
|
1664 |
+
role="user",
|
1665 |
+
content=user_message + "/n (請一定要用繁體中文回答 zh-TW,並用台灣人的禮貌口語表達,回答時不要特別說明這是台灣人的語氣,不用提到「逐字稿」這個詞,用「內容」代替),回答時請用數學符號代替文字(Latex 用 $ 字號 render)"
|
1666 |
+
)
|
1667 |
|
1668 |
+
# 运行助手,生成响应
|
1669 |
+
run = client.beta.threads.runs.create(
|
1670 |
+
thread_id=thread.id,
|
1671 |
+
assistant_id=assistant_id,
|
1672 |
+
instructions=instructions,
|
1673 |
+
)
|
|
|
|
|
|
|
1674 |
|
1675 |
+
# 等待助手响应,设定最大等待时间为 30 秒
|
1676 |
+
run_status = poll_run_status(run.id, thread.id, timeout=30)
|
1677 |
+
# 获取助手的响应消息
|
1678 |
+
if run_status == "completed":
|
1679 |
+
messages = client.beta.threads.messages.list(thread_id=thread.id)
|
1680 |
+
# [MessageContentText(text=Text(annotations=[], value='您好!有什麼我可以幫助您的嗎?如果有任何問題或需要指導,請隨時告訴我!'), type='text')]
|
1681 |
+
response_text = messages.data[0].content[0].text.value
|
1682 |
+
else:
|
1683 |
+
response_text = "學習精靈有點累,請稍後再試!"
|
1684 |
+
|
1685 |
+
# 更新聊天历史
|
1686 |
+
new_chat_history = (user_message, response_text)
|
1687 |
+
if chat_history is None:
|
1688 |
+
chat_history = [new_chat_history]
|
1689 |
+
else:
|
1690 |
+
chat_history.append(new_chat_history)
|
1691 |
+
except Exception as e:
|
1692 |
+
print(f"Error: {e}")
|
1693 |
+
raise gr.Error(f"Error: {e}")
|
1694 |
|
1695 |
# 返回聊天历史和空字符串清空输入框
|
1696 |
return "", chat_history, thread.id
|
1697 |
|
1698 |
+
def process_open_ai_audio_to_chatbot(password, audio_url):
|
1699 |
+
verify_password(password)
|
1700 |
+
if audio_url:
|
1701 |
+
with open(audio_url, "rb") as audio_file:
|
1702 |
+
file_size = os.path.getsize(audio_url)
|
1703 |
+
if file_size > 2000000:
|
1704 |
+
raise gr.Error("檔案大小超過,請不要超過 60秒")
|
1705 |
+
else:
|
1706 |
+
response = OPEN_AI_CLIENT.audio.transcriptions.create(
|
1707 |
+
model="whisper-1",
|
1708 |
+
file=audio_file,
|
1709 |
+
response_format="text"
|
1710 |
+
)
|
1711 |
+
# response 拆解 dict
|
1712 |
+
print("=== response ===")
|
1713 |
+
print(response)
|
1714 |
+
print("=== response ===")
|
1715 |
+
else:
|
1716 |
+
response = ""
|
1717 |
+
|
1718 |
+
return response
|
1719 |
+
|
1720 |
def poll_run_status(run_id, thread_id, timeout=600, poll_interval=5):
|
1721 |
"""
|
1722 |
Polls the status of a Run and handles different statuses appropriately.
|
|
|
1766 |
|
1767 |
return run.status
|
1768 |
|
1769 |
+
# --- Slide mode ---
|
1770 |
def update_slide(direction):
|
1771 |
global TRANSCRIPTS
|
1772 |
global CURRENT_INDEX
|
|
|
1794 |
def next_slide():
|
1795 |
return update_slide(1)
|
1796 |
|
1797 |
+
def init_params(text, request: gr.Request):
|
1798 |
+
if request:
|
1799 |
+
print("Request headers dictionary:", request.headers)
|
1800 |
+
print("IP address:", request.client.host)
|
1801 |
+
print("Query parameters:", dict(request.query_params))
|
1802 |
+
# url = request.url
|
1803 |
+
print("Request URL:", request.url)
|
1804 |
+
|
1805 |
+
youtube_link = ""
|
1806 |
+
password_text = ""
|
1807 |
+
admin = gr.update(visible=True)
|
1808 |
+
reading_passage_admin = gr.update(visible=True)
|
1809 |
+
summary_admin = gr.update(visible=True)
|
1810 |
+
see_detail = gr.update(visible=True)
|
1811 |
+
|
1812 |
+
# if youtube_link in query_params
|
1813 |
+
if "youtube_id" in request.query_params:
|
1814 |
+
youtube_id = request.query_params["youtube_id"]
|
1815 |
+
youtube_link = f"https://www.youtube.com/watch?v={youtube_id}"
|
1816 |
+
print(f"youtube_link: {youtube_link}")
|
1817 |
+
|
1818 |
+
# check if origin is from junyiacademy
|
1819 |
+
origin = request.headers.get("origin", "")
|
1820 |
+
if "junyiacademy" in origin:
|
1821 |
+
password_text = "6161"
|
1822 |
+
admin = gr.update(visible=False)
|
1823 |
+
reading_passage_admin = gr.update(visible=False)
|
1824 |
+
summary_admin = gr.update(visible=False)
|
1825 |
+
see_detail = gr.update(visible=False)
|
1826 |
+
|
1827 |
+
return admin, reading_passage_admin, summary_admin, see_detail, password_text, youtube_link
|
1828 |
|
1829 |
HEAD = """
|
1830 |
<meta charset="UTF-8">
|
|
|
1851 |
});
|
1852 |
}
|
1853 |
</script>
|
|
|
1854 |
|
1855 |
+
<script>
|
1856 |
+
function changeImage(direction, count, galleryIndex) {
|
1857 |
+
// Find the current visible image by iterating over possible indices
|
1858 |
+
var currentImage = null;
|
1859 |
+
var currentIndex = -1;
|
1860 |
+
for (var i = 0; i < count; i++) {
|
1861 |
+
var img = document.querySelector('.slide-image-' + galleryIndex + '-' + i);
|
1862 |
+
if (img && img.style.display !== 'none') {
|
1863 |
+
currentImage = img;
|
1864 |
+
currentIndex = i;
|
1865 |
+
break;
|
1866 |
+
}
|
1867 |
+
}
|
1868 |
+
|
1869 |
+
// If no current image is visible, show the first one and return
|
1870 |
+
if (currentImage === null) {
|
1871 |
+
document.querySelector('.slide-image-' + galleryIndex + '-0').style.display = 'block';
|
1872 |
+
console.error('No current image found for galleryIndex ' + galleryIndex + ', defaulting to first image.');
|
1873 |
+
return;
|
1874 |
+
}
|
1875 |
+
|
1876 |
+
// Hide the current image
|
1877 |
+
currentImage.style.display = 'none';
|
1878 |
+
|
1879 |
+
// Calculate the index of the next image to show
|
1880 |
+
var newIndex = (currentIndex + direction + count) % count;
|
1881 |
+
|
1882 |
+
// Select the next image and show it
|
1883 |
+
var nextImage = document.querySelector('.slide-image-' + galleryIndex + '-' + newIndex);
|
1884 |
+
if (nextImage) {
|
1885 |
+
nextImage.style.display = 'block';
|
1886 |
+
} else {
|
1887 |
+
console.error('No image found for galleryIndex ' + galleryIndex + ' and newIndex ' + newIndex);
|
1888 |
+
}
|
1889 |
+
}
|
1890 |
+
</script>
|
1891 |
+
"""
|
1892 |
|
1893 |
+
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:
|
1894 |
+
with gr.Row() as admin:
|
1895 |
+
password = gr.Textbox(label="Password", type="password", elem_id="password_input", visible=True)
|
1896 |
+
file_upload = gr.File(label="Upload your CSV or Word file", visible=False)
|
1897 |
+
youtube_link = gr.Textbox(label="Enter YouTube Link", elem_id="youtube_link_input", visible=True)
|
1898 |
+
video_id = gr.Textbox(label="video_id", visible=False)
|
1899 |
+
web_link = gr.Textbox(label="Enter Web Page Link", visible=False)
|
1900 |
+
user_data = gr.Textbox(label="User Data", elem_id="user_data_input", visible=True)
|
1901 |
+
youtube_link_btn = gr.Button("Submit_YouTube_Link", elem_id="youtube_link_btn", visible=True)
|
1902 |
+
with gr.Tab("AI小精靈"):
|
1903 |
+
with gr.Row():
|
1904 |
+
with gr.Tab("飛特"):
|
1905 |
+
bot_avatar = "https://junyi-avatar.s3.ap-northeast-1.amazonaws.com/live/%20%20foxcat-star-18.png?v=20231113095823614"
|
1906 |
+
user_avatar = "https://junyitopicimg.s3.amazonaws.com/s4byy--icon.jpe?v=20200513013523726"
|
1907 |
+
latex_delimiters = [{"left": "$", "right": "$", "display": False}]
|
1908 |
+
chatbot = gr.Chatbot(avatar_images=[bot_avatar, user_avatar], label="OPEN AI", show_share_button=False, likeable=True, show_label=False, latex_delimiters=latex_delimiters)
|
1909 |
+
thread_id = gr.Textbox(label="thread_id", visible=False)
|
1910 |
+
socratic_mode_btn = gr.Checkbox(label="蘇格拉底家教助理模式", value=True, visible=False)
|
|
|
|
|
1911 |
with gr.Row():
|
1912 |
+
with gr.Accordion("你也有類似的問題想問嗎?", open=False) as ask_questions_accordion:
|
1913 |
+
btn_1 = gr.Button("問題一")
|
1914 |
+
btn_2 = gr.Button("問題一")
|
1915 |
+
btn_3 = gr.Button("問題一")
|
1916 |
+
gr.Markdown("### 重新生成問題")
|
1917 |
+
btn_create_question = gr.Button("生成其他問題", variant="primary")
|
1918 |
+
openai_chatbot_audio_input = gr.Audio(sources=["microphone"], type="filepath")
|
1919 |
+
with gr.Row():
|
1920 |
+
msg = gr.Textbox(label="訊息",scale=3)
|
1921 |
+
send_button = gr.Button("送出", variant="primary", scale=1)
|
1922 |
+
# with gr.Tab("GROQ"):
|
1923 |
+
# groq_ai_name = gr.Textbox(label="AI 助理名稱", value="groq", visible=False)
|
1924 |
+
# groq_chatbot = gr.Chatbot(avatar_images=[bot_avatar, user_avatar], label="groq mode chatbot", show_share_button=False, likeable=True)
|
1925 |
+
# groq_msg = gr.Textbox(label="Message")
|
1926 |
+
# groq_send_button = gr.Button("Send", variant="primary")
|
1927 |
+
# with gr.Tab("JUTOR"):
|
1928 |
+
# jutor_ai_name = gr.Textbox(label="AI 助理名稱", value="jutor", visible=False)
|
1929 |
+
# jutor_chatbot = gr.Chatbot(avatar_images=[bot_avatar, user_avatar], label="jutor mode chatbot", show_share_button=False, likeable=True)
|
1930 |
+
# jutor_msg = gr.Textbox(label="Message")
|
1931 |
+
# jutor_send_button = gr.Button("Send", variant="primary")
|
1932 |
+
# with gr.Tab("CLAUDE"):
|
1933 |
+
# claude_ai_name = gr.Textbox(label="AI 助理名稱", value="claude3", visible=False)
|
1934 |
+
# claude_chatbot = gr.Chatbot(avatar_images=[bot_avatar, user_avatar], label="claude mode chatbot", show_share_button=False, likeable=True)
|
1935 |
+
# claude_msg = gr.Textbox(label="Message")
|
1936 |
+
# claude_send_button = gr.Button("Send", variant="primary")
|
1937 |
+
with gr.Tab("其他精靈"):
|
1938 |
+
ai_name = gr.Dropdown(label="選擇 AI 助理", choices=["jutor", "claude3", "groq"], value="jutor")
|
1939 |
+
ai_chatbot = gr.Chatbot(avatar_images=[bot_avatar, user_avatar], label="ai_chatbot", show_share_button=False, likeable=True, show_label=False)
|
1940 |
+
ai_msg = gr.Textbox(label="Message")
|
1941 |
+
ai_send_button = gr.Button("Send", variant="primary")
|
1942 |
+
with gr.Tab("文章模式"):
|
1943 |
+
with gr.Row() as reading_passage_admin:
|
1944 |
+
reading_passage_kind = gr.Textbox(value="reading_passage", show_label=False)
|
1945 |
+
reading_passage_edit_button = gr.Button("編輯", size="sm", variant="primary")
|
1946 |
+
reading_passage_update_button = gr.Button("更新", size="sm", variant="primary")
|
1947 |
+
reading_passage_delete_button = gr.Button("刪除", size="sm", variant="primary")
|
1948 |
+
reading_passage_create_button = gr.Button("建立", size="sm", variant="primary")
|
1949 |
+
with gr.Row():
|
1950 |
+
reading_passage = gr.Textbox(label="Reading Passage", lines=40, show_label=False)
|
1951 |
+
reading_passage_speak_button = gr.Button("Speak", visible=False)
|
1952 |
+
reading_passage_audio_output = gr.Audio(label="Audio Output", visible=False)
|
1953 |
+
with gr.Tab("重點摘要"):
|
1954 |
+
with gr.Row() as summary_admmin:
|
1955 |
+
summary_kind = gr.Textbox(value="summary", show_label=False)
|
1956 |
+
summary_edit_button = gr.Button("編輯", size="sm", variant="primary")
|
1957 |
+
summary_update_button = gr.Button("更新", size="sm", variant="primary")
|
1958 |
+
summary_delete_button = gr.Button("刪除", size="sm", variant="primary")
|
1959 |
+
summary_create_button = gr.Button("建立", size="sm", variant="primary")
|
1960 |
+
with gr.Row():
|
1961 |
+
df_summarise = gr.Textbox(container=True, show_copy_button=True, lines=40, show_label=False)
|
1962 |
+
with gr.Tab("關鍵時刻"):
|
1963 |
+
with gr.Row():
|
1964 |
+
key_moments_html = gr.HTML(value="")
|
1965 |
+
with gr.Tab("教學備課"):
|
1966 |
+
with gr.Row():
|
1967 |
+
content_subject = gr.Dropdown(label="選擇主題", choices=["數學", "自然", "國文", "英文", "社會","物理", "化學", "生物", "地理", "歷史", "公民"], value="", visible=False)
|
1968 |
+
content_grade = gr.Dropdown(label="選擇年級", choices=["一年級", "二年級", "三年級", "四年級", "五年級", "六年級", "七年級", "八年級", "九年級", "十年級", "十一年級", "十二年級"], value="", visible=False)
|
1969 |
+
content_level = gr.Dropdown(label="差異化教學", choices=["基礎", "中級", "進階"], value="基礎")
|
1970 |
+
with gr.Row():
|
1971 |
with gr.Tab("學習單"):
|
1972 |
+
with gr.Row():
|
1973 |
+
with gr.Column(scale=1):
|
1974 |
+
with gr.Row():
|
1975 |
+
worksheet_content_type_name = gr.Textbox(value="worksheet", visible=False)
|
1976 |
+
worksheet_algorithm = gr.Dropdown(label="選擇教學策略或理論", choices=["Bloom認知階層理論", "Polya數學解題法", "CRA教學法"], value="Bloom認知階層理論", visible=False)
|
1977 |
+
worksheet_content_btn = gr.Button("生成學習單 📄", variant="primary")
|
1978 |
+
with gr.Accordion("微調", open=False):
|
1979 |
+
worksheet_exam_result_fine_tune_prompt = gr.Textbox(label="根據結果,輸入你想更改的想法")
|
1980 |
+
worksheet_exam_result_fine_tune_btn = gr.Button("微調結果", variant="primary")
|
1981 |
+
worksheet_exam_result_retrun_original = gr.Button("返回原始結果")
|
1982 |
+
with gr.Accordion("prompt", open=False):
|
1983 |
+
worksheet_prompt = gr.Textbox(label="worksheet_prompt", show_copy_button=True, lines=40)
|
1984 |
+
with gr.Column(scale=2):
|
1985 |
+
# 生成對應不同模式的結果
|
1986 |
+
worksheet_exam_result_prompt = gr.Textbox(visible=False)
|
1987 |
+
worksheet_exam_result_original = gr.Textbox(visible=False)
|
1988 |
+
worksheet_exam_result = gr.Textbox(label="初次生成結果", show_copy_button=True, interactive=True, lines=40)
|
1989 |
+
worksheet_download_exam_result_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
|
1990 |
+
worksheet_exam_result_word_link = gr.File(label="Download Word")
|
1991 |
+
|
1992 |
+
with gr.Tab("課程計畫"):
|
1993 |
+
with gr.Row():
|
1994 |
+
with gr.Column(scale=1):
|
1995 |
+
with gr.Row():
|
1996 |
+
lesson_plan_content_type_name = gr.Textbox(value="lesson_plan", visible=False)
|
1997 |
+
lesson_plan_time = gr.Slider(label="選擇課程時間(分鐘)", minimum=10, maximum=120, step=5, value=40)
|
1998 |
+
lesson_plan_btn = gr.Button("生成課程計畫 📕", variant="primary")
|
1999 |
+
with gr.Accordion("微調", open=False):
|
2000 |
+
lesson_plan_exam_result_fine_tune_prompt = gr.Textbox(label="根據結果,輸入你想更改的想法")
|
2001 |
+
lesson_plan_exam_result_fine_tune_btn = gr.Button("微調結果", variant="primary")
|
2002 |
+
lesson_plan_exam_result_retrun_original = gr.Button("返回原始結果")
|
2003 |
+
with gr.Accordion("prompt", open=False):
|
2004 |
+
lesson_plan_prompt = gr.Textbox(label="worksheet_prompt", show_copy_button=True, lines=40)
|
2005 |
+
with gr.Column(scale=2):
|
2006 |
+
# 生成對應不同模式的結果
|
2007 |
+
lesson_plan_exam_result_prompt = gr.Textbox(visible=False)
|
2008 |
+
lesson_plan_exam_result_original = gr.Textbox(visible=False)
|
2009 |
+
lesson_plan_exam_result = gr.Textbox(label="初次生成結果", show_copy_button=True, interactive=True, lines=40)
|
2010 |
+
|
2011 |
+
lesson_plan_download_exam_result_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
|
2012 |
+
lesson_plan_exam_result_word_link = gr.File(label="Download Word")
|
2013 |
|
2014 |
+
with gr.Tab("出場券"):
|
2015 |
+
with gr.Row():
|
2016 |
+
with gr.Column(scale=1):
|
2017 |
+
with gr.Row():
|
2018 |
+
exit_ticket_content_type_name = gr.Textbox(value="exit_ticket", visible=False)
|
2019 |
+
exit_ticket_time = gr.Slider(label="選擇出場券時間(分鐘)", minimum=5, maximum=10, step=1, value=8)
|
2020 |
+
exit_ticket_btn = gr.Button("生成出場券 🎟️", variant="primary")
|
2021 |
+
with gr.Accordion("微調", open=False):
|
2022 |
+
exit_ticket_exam_result_fine_tune_prompt = gr.Textbox(label="根據結果,輸入你想更改的想法")
|
2023 |
+
exit_ticket_exam_result_fine_tune_btn = gr.Button("微調結果", variant="primary")
|
2024 |
+
exit_ticket_exam_result_retrun_original = gr.Button("返回原始結果")
|
2025 |
+
with gr.Accordion("prompt", open=False):
|
2026 |
+
exit_ticket_prompt = gr.Textbox(label="worksheet_prompt", show_copy_button=True, lines=40)
|
2027 |
+
with gr.Column(scale=2):
|
2028 |
+
# 生成對應不同模式的結果
|
2029 |
+
exit_ticket_exam_result_prompt = gr.Textbox(visible=False)
|
2030 |
+
exit_ticket_exam_result_original = gr.Textbox(visible=False)
|
2031 |
+
exit_ticket_exam_result = gr.Textbox(label="初次生成結果", show_copy_button=True, interactive=True, lines=40)
|
2032 |
+
|
2033 |
+
exit_ticket_download_exam_result_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
|
2034 |
+
exit_ticket_exam_result_word_link = gr.File(label="Download Word")
|
2035 |
+
|
2036 |
+
|
2037 |
+
# with gr.Tab("素養導向閱讀題組"):
|
2038 |
+
# literacy_oriented_reading_content = gr.Textbox(label="輸入閱讀材料")
|
2039 |
+
# literacy_oriented_reading_content_btn = gr.Button("生成閱讀理解題")
|
2040 |
+
|
2041 |
+
# with gr.Tab("自我評估"):
|
2042 |
+
# self_assessment_content = gr.Textbox(label="輸入自評問卷或檢查表")
|
2043 |
+
# self_assessment_content_btn = gr.Button("生成自評問卷")
|
2044 |
+
# with gr.Tab("自我反思評量"):
|
2045 |
+
# self_reflection_content = gr.Textbox(label="輸入自我反思活動")
|
2046 |
+
# self_reflection_content_btn = gr.Button("生成自我反思活動")
|
2047 |
+
# with gr.Tab("後設認知"):
|
2048 |
+
# metacognition_content = gr.Textbox(label="輸入後設認知相關問題")
|
2049 |
+
# metacognition_content_btn = gr.Button("生成後設認知問題")
|
2050 |
+
|
2051 |
+
with gr.Accordion("See Details", open=False) as see_details:
|
2052 |
+
with gr.Tab("本文"):
|
2053 |
+
df_string_output = gr.Textbox(lines=40, label="Data Text")
|
2054 |
+
with gr.Tab("逐字稿"):
|
2055 |
+
simple_html_content = gr.HTML(label="Simple Transcript")
|
2056 |
+
with gr.Tab("圖文"):
|
2057 |
+
transcript_html = gr.HTML(label="YouTube Transcript and Video")
|
2058 |
+
with gr.Tab("投影片"):
|
2059 |
+
slide_image = gr.Image()
|
2060 |
+
slide_text = gr.Textbox()
|
2061 |
+
with gr.Row():
|
2062 |
+
prev_button = gr.Button("Previous")
|
2063 |
+
next_button = gr.Button("Next")
|
2064 |
+
prev_button.click(fn=prev_slide, inputs=[], outputs=[slide_image, slide_text])
|
2065 |
+
next_button.click(fn=next_slide, inputs=[], outputs=[slide_image, slide_text])
|
2066 |
+
with gr.Tab("markdown"):
|
2067 |
+
gr.Markdown("## 請複製以下 markdown 並貼到你的心智圖工具中,建議使用:https://markmap.js.org/repl")
|
2068 |
+
mind_map = gr.Textbox(container=True, show_copy_button=True, lines=40, elem_id="mind_map_markdown")
|
2069 |
+
with gr.Tab("心智圖",elem_id="mind_map_tab"):
|
2070 |
+
mind_map_html = gr.HTML()
|
2071 |
+
|
2072 |
+
# --- Event ---
|
2073 |
+
# OPENAI 模式
|
2074 |
send_button.click(
|
2075 |
+
chat_with_opan_ai_assistant,
|
2076 |
+
inputs=[password, video_id, thread_id, df_string_output, msg, chatbot, content_subject, content_grade, socratic_mode_btn],
|
2077 |
outputs=[msg, chatbot, thread_id]
|
2078 |
)
|
2079 |
+
openai_chatbot_audio_input.change(
|
2080 |
+
process_open_ai_audio_to_chatbot,
|
2081 |
+
inputs=[password, openai_chatbot_audio_input],
|
2082 |
+
outputs=[msg]
|
2083 |
+
)
|
2084 |
+
# # GROQ 模式
|
2085 |
+
# groq_send_button.click(
|
2086 |
+
# chat_with_ai,
|
2087 |
+
# inputs=[groq_ai_name, password, video_id, df_string_output, groq_msg, groq_chatbot, content_subject, content_grade, socratic_mode_btn],
|
2088 |
+
# outputs=[groq_msg, groq_chatbot]
|
2089 |
+
# )
|
2090 |
+
# # JUTOR API 模式
|
2091 |
+
# jutor_send_button.click(
|
2092 |
+
# chat_with_ai,
|
2093 |
+
# inputs=[jutor_ai_name, password, video_id, df_string_output, jutor_msg, jutor_chatbot, content_subject, content_grade, socratic_mode_btn],
|
2094 |
+
# outputs=[jutor_msg, jutor_chatbot]
|
2095 |
+
# )
|
2096 |
+
# # CLAUDE 模式
|
2097 |
+
# claude_send_button.click(
|
2098 |
+
# chat_with_ai,
|
2099 |
+
# inputs=[claude_ai_name, password, video_id, df_string_output, claude_msg, claude_chatbot, content_subject, content_grade, socratic_mode_btn],
|
2100 |
+
# outputs=[claude_msg, claude_chatbot]
|
2101 |
+
# )
|
2102 |
+
# ai_chatbot 模式
|
2103 |
+
ai_send_button.click(
|
2104 |
+
chat_with_ai,
|
2105 |
+
inputs=[ai_name, password, video_id, df_string_output, ai_msg, ai_chatbot, content_subject, content_grade, socratic_mode_btn],
|
2106 |
+
outputs=[ai_msg, ai_chatbot]
|
2107 |
+
)
|
2108 |
+
|
2109 |
# 连接按钮点击事件
|
2110 |
+
btn_1_chat_with_opan_ai_assistant_input =[password, video_id, thread_id, df_string_output, btn_1, chatbot, content_subject, content_grade, socratic_mode_btn]
|
2111 |
+
btn_2_chat_with_opan_ai_assistant_input =[password, video_id, thread_id, df_string_output, btn_2, chatbot, content_subject, content_grade, socratic_mode_btn]
|
2112 |
+
btn_3_chat_with_opan_ai_assistant_input =[password, video_id, thread_id, df_string_output, btn_3, chatbot, content_subject, content_grade, socratic_mode_btn]
|
2113 |
btn_1.click(
|
2114 |
+
chat_with_opan_ai_assistant,
|
2115 |
+
inputs=btn_1_chat_with_opan_ai_assistant_input,
|
2116 |
outputs=[msg, chatbot, thread_id]
|
2117 |
)
|
2118 |
btn_2.click(
|
2119 |
+
chat_with_opan_ai_assistant,
|
2120 |
+
inputs=btn_2_chat_with_opan_ai_assistant_input,
|
2121 |
outputs=[msg, chatbot, thread_id]
|
2122 |
)
|
2123 |
btn_3.click(
|
2124 |
+
chat_with_opan_ai_assistant,
|
2125 |
+
inputs=btn_3_chat_with_opan_ai_assistant_input,
|
2126 |
outputs=[msg, chatbot, thread_id]
|
2127 |
)
|
2128 |
|
2129 |
+
btn_create_question.click(
|
2130 |
+
change_questions,
|
2131 |
+
inputs = [password, df_string_output],
|
2132 |
+
outputs = [btn_1, btn_2, btn_3]
|
2133 |
+
)
|
2134 |
|
2135 |
# file_upload.change(process_file, inputs=file_upload, outputs=df_string_output)
|
2136 |
file_upload.change(process_file, inputs=file_upload, outputs=[btn_1, btn_2, btn_3, df_summarise, df_string_output])
|
2137 |
|
2138 |
# 当输入 YouTube 链接时触发
|
2139 |
+
process_youtube_link_output = [
|
2140 |
+
video_id,
|
2141 |
+
btn_1,
|
2142 |
+
btn_2,
|
2143 |
+
btn_3,
|
2144 |
+
df_string_output,
|
2145 |
+
df_summarise,
|
2146 |
+
key_moments_html,
|
2147 |
+
mind_map,
|
2148 |
+
mind_map_html,
|
2149 |
+
transcript_html,
|
2150 |
+
simple_html_content,
|
2151 |
+
slide_image,
|
2152 |
+
slide_text,
|
2153 |
+
reading_passage,
|
2154 |
+
content_subject,
|
2155 |
+
content_grade,
|
2156 |
+
]
|
2157 |
youtube_link.change(
|
2158 |
process_youtube_link,
|
2159 |
+
inputs=[password,youtube_link],
|
2160 |
+
outputs=process_youtube_link_output
|
2161 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2162 |
|
2163 |
youtube_link_btn.click(
|
2164 |
process_youtube_link,
|
2165 |
+
inputs=[password, youtube_link],
|
2166 |
+
outputs=process_youtube_link_output
|
2167 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2168 |
|
2169 |
# 当输入网页链接时触发
|
2170 |
# web_link.change(process_web_link, inputs=web_link, outputs=[btn_1, btn_2, btn_3, df_summarise, df_string_output])
|
2171 |
|
2172 |
+
# reading_passage event
|
2173 |
+
reading_passage_create_button.click(
|
2174 |
+
create_LLM_content,
|
2175 |
+
inputs=[video_id, df_string_output, reading_passage_kind],
|
2176 |
+
outputs=[reading_passage]
|
2177 |
+
)
|
2178 |
+
reading_passage_delete_button.click(
|
2179 |
+
delete_LLM_content,
|
2180 |
+
inputs=[video_id, reading_passage_kind],
|
2181 |
+
outputs=[reading_passage]
|
2182 |
+
)
|
2183 |
+
reading_passage_edit_button.click(
|
2184 |
+
enable_edit_mode,
|
2185 |
+
inputs=[],
|
2186 |
+
outputs=[reading_passage]
|
2187 |
+
)
|
2188 |
+
reading_passage_update_button.click(
|
2189 |
+
update_LLM_content,
|
2190 |
+
inputs=[video_id, reading_passage, reading_passage_kind],
|
2191 |
+
outputs=[reading_passage]
|
2192 |
+
)
|
2193 |
+
|
2194 |
+
# summary event
|
2195 |
+
summary_create_button.click(
|
2196 |
+
create_LLM_content,
|
2197 |
+
inputs=[video_id, df_string_output, summary_kind],
|
2198 |
+
outputs=[df_summarise]
|
2199 |
+
)
|
2200 |
+
summary_delete_button.click(
|
2201 |
+
delete_LLM_content,
|
2202 |
+
inputs=[video_id, summary_kind],
|
2203 |
+
outputs=[df_summarise]
|
2204 |
+
)
|
2205 |
+
summary_edit_button.click(
|
2206 |
+
enable_edit_mode,
|
2207 |
+
inputs=[],
|
2208 |
+
outputs=[df_summarise]
|
2209 |
+
)
|
2210 |
+
summary_update_button.click(
|
2211 |
+
update_LLM_content,
|
2212 |
+
inputs=[video_id, df_summarise, summary_kind],
|
2213 |
+
outputs=[df_summarise]
|
2214 |
+
)
|
2215 |
+
|
2216 |
+
# 教師版
|
2217 |
+
worksheet_content_btn.click(
|
2218 |
+
get_ai_content,
|
2219 |
+
inputs=[password, video_id, df_string_output, content_subject, content_grade, content_level, worksheet_algorithm, worksheet_content_type_name],
|
2220 |
+
outputs=[worksheet_exam_result_original, worksheet_exam_result, worksheet_prompt, worksheet_exam_result_prompt]
|
2221 |
+
)
|
2222 |
+
lesson_plan_btn.click(
|
2223 |
+
get_ai_content,
|
2224 |
+
inputs=[password, video_id, df_string_output, content_subject, content_grade, content_level, lesson_plan_time, lesson_plan_content_type_name],
|
2225 |
+
outputs=[lesson_plan_exam_result_original, lesson_plan_exam_result, lesson_plan_prompt, lesson_plan_exam_result_prompt]
|
2226 |
+
)
|
2227 |
+
exit_ticket_btn.click(
|
2228 |
+
get_ai_content,
|
2229 |
+
inputs=[password, video_id, df_string_output, content_subject, content_grade, content_level, exit_ticket_time, exit_ticket_content_type_name],
|
2230 |
+
outputs=[exit_ticket_exam_result_original, exit_ticket_exam_result, exit_ticket_prompt, exit_ticket_exam_result_prompt]
|
2231 |
+
)
|
2232 |
+
|
2233 |
+
# 生成結果微調
|
2234 |
+
worksheet_exam_result_fine_tune_btn.click(
|
2235 |
+
generate_exam_fine_tune_result,
|
2236 |
+
inputs=[password, worksheet_exam_result_prompt, df_string_output, worksheet_exam_result, worksheet_exam_result_fine_tune_prompt],
|
2237 |
+
outputs=[worksheet_exam_result]
|
2238 |
+
)
|
2239 |
+
worksheet_download_exam_result_button.click(
|
2240 |
+
download_exam_result,
|
2241 |
+
inputs=[worksheet_exam_result],
|
2242 |
+
outputs=[worksheet_exam_result_word_link]
|
2243 |
+
)
|
2244 |
+
worksheet_exam_result_retrun_original.click(
|
2245 |
+
return_original_exam_result,
|
2246 |
+
inputs=[worksheet_exam_result_original],
|
2247 |
+
outputs=[worksheet_exam_result]
|
2248 |
+
)
|
2249 |
+
lesson_plan_exam_result_fine_tune_btn.click(
|
2250 |
+
generate_exam_fine_tune_result,
|
2251 |
+
inputs=[password, lesson_plan_exam_result_prompt, df_string_output, lesson_plan_exam_result, lesson_plan_exam_result_fine_tune_prompt],
|
2252 |
+
outputs=[lesson_plan_exam_result]
|
2253 |
+
)
|
2254 |
+
lesson_plan_download_exam_result_button.click(
|
2255 |
+
download_exam_result,
|
2256 |
+
inputs=[lesson_plan_exam_result],
|
2257 |
+
outputs=[lesson_plan_exam_result_word_link]
|
2258 |
+
)
|
2259 |
+
lesson_plan_exam_result_retrun_original.click(
|
2260 |
+
return_original_exam_result,
|
2261 |
+
inputs=[lesson_plan_exam_result_original],
|
2262 |
+
outputs=[lesson_plan_exam_result]
|
2263 |
+
)
|
2264 |
+
exit_ticket_exam_result_fine_tune_btn.click(
|
2265 |
+
generate_exam_fine_tune_result,
|
2266 |
+
inputs=[password, exit_ticket_exam_result_prompt, df_string_output, exit_ticket_exam_result, exit_ticket_exam_result_fine_tune_prompt],
|
2267 |
+
outputs=[exit_ticket_exam_result]
|
2268 |
+
)
|
2269 |
+
exit_ticket_download_exam_result_button.click(
|
2270 |
+
download_exam_result,
|
2271 |
+
inputs=[exit_ticket_exam_result],
|
2272 |
+
outputs=[exit_ticket_exam_result_word_link]
|
2273 |
+
)
|
2274 |
+
exit_ticket_exam_result_retrun_original.click(
|
2275 |
+
return_original_exam_result,
|
2276 |
+
inputs=[exit_ticket_exam_result_original],
|
2277 |
+
outputs=[exit_ticket_exam_result]
|
2278 |
+
)
|
2279 |
+
|
2280 |
+
# init_params
|
2281 |
+
demo.load(
|
2282 |
+
init_params,
|
2283 |
+
inputs =[youtube_link],
|
2284 |
+
outputs = [admin, reading_passage_admin, summary_admmin, see_details, password , youtube_link]
|
2285 |
+
)
|
2286 |
+
|
2287 |
demo.launch(allowed_paths=["videos"])
|
chatbot.py
ADDED
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import json
|
3 |
+
import requests
|
4 |
+
|
5 |
+
class Chatbot:
|
6 |
+
def __init__(self, config):
|
7 |
+
self.video_id = config.get('video_id')
|
8 |
+
self.content_subject = config.get('content_subject')
|
9 |
+
self.content_grade = config.get('content_grade')
|
10 |
+
self.jutor_chat_key = config.get('jutor_chat_key')
|
11 |
+
self.transcript_text = self.get_transcript_text(config.get('trascript'))
|
12 |
+
self.ai_name = config.get('ai_name')
|
13 |
+
self.ai_client = config.get('ai_client')
|
14 |
+
|
15 |
+
def get_transcript_text(self, transcript_data):
|
16 |
+
transcript_json = json.loads(transcript_data)
|
17 |
+
for entry in transcript_json:
|
18 |
+
entry.pop('embed_url', None)
|
19 |
+
entry.pop('screenshot_path', None)
|
20 |
+
transcript_text = json.dumps(transcript_json, ensure_ascii=False)
|
21 |
+
return transcript_text
|
22 |
+
|
23 |
+
def chat(self, user_message, chat_history, socratic_mode=False, service_type='jutor'):
|
24 |
+
messages = self.prepare_messages(chat_history, user_message)
|
25 |
+
system_prompt = self.prepare_system_prompt(socratic_mode)
|
26 |
+
if service_type in ['jutor', 'groq', 'claude3']:
|
27 |
+
response_text = self.chat_with_service(service_type, system_prompt, messages)
|
28 |
+
return response_text
|
29 |
+
else:
|
30 |
+
raise gr.Error("不支持此服務")
|
31 |
+
|
32 |
+
def prepare_system_prompt(self, socratic_mode):
|
33 |
+
content_subject = self.content_subject
|
34 |
+
content_grade = self.content_grade
|
35 |
+
video_id = self.video_id
|
36 |
+
trascript_text = self.transcript_text
|
37 |
+
socratic_mode = str(socratic_mode)
|
38 |
+
ai_name = self.ai_name
|
39 |
+
system_prompt = f"""
|
40 |
+
科目:{content_subject}
|
41 |
+
年級:{content_grade}
|
42 |
+
逐字稿資料:{trascript_text}
|
43 |
+
-------------------------------------
|
44 |
+
你是一個專業的{content_subject}老師, user 為{content_grade}的學生
|
45 |
+
socratic_mode = {socratic_mode}
|
46 |
+
if socratic_mode is True,
|
47 |
+
- 請用蘇格拉底式的提問方式,引導學生思考,並且給予學生一些提示
|
48 |
+
- 一次只問一個問題,字數在100字以內
|
49 |
+
- 不要直接給予答案,讓學生自己思考
|
50 |
+
- 但可以給予一些提示跟引導,例如給予影片的時間軸,讓學生自己去找答案
|
51 |
+
|
52 |
+
if socratic_mode is False,
|
53 |
+
- 直接回答學生問題,字數在100字以內
|
54 |
+
|
55 |
+
rule:
|
56 |
+
- 請一定要用繁體中文回答 zh-TW,並用台灣人的口語表達,回答時不用特別說明這是台灣人的語氣,也不用說這是「台語的說法」
|
57 |
+
- 不用提到「逐字稿」這個詞
|
58 |
+
- 如果學生問了一些問題你無法判斷,請告訴學生你無法判斷,並建議學生可以問其他問題
|
59 |
+
- 或者你可以反問學生一些問題,幫助學生更好的理解資料,字數在100字以內
|
60 |
+
- 如果學生的問題與資料文本無關,請告訴學生你「無法回答超出影片範圍的問題」,並告訴他可以怎麼問什麼樣的問題(一個就好)
|
61 |
+
- 只要是參考逐字稿資料,請在回答的最後標註【參考資料:(分):(秒)】
|
62 |
+
- 回答範圍一定要在逐字稿資料內,不要引用其他資料,請嚴格執行
|
63 |
+
- 並在重複問句後給予學生鼓勵,讓學生有學習的動力
|
64 |
+
- 請用 {content_grade} 的學生能懂的方式回答
|
65 |
+
- 回答時數學式請用數學符號代替文字(Latex 用 $ 字號 render)
|
66 |
+
"""
|
67 |
+
|
68 |
+
return system_prompt
|
69 |
+
|
70 |
+
def prepare_messages(self, chat_history, user_message):
|
71 |
+
messages = []
|
72 |
+
if chat_history is not None:
|
73 |
+
if len(chat_history) > 10:
|
74 |
+
chat_history = chat_history[-10:]
|
75 |
+
|
76 |
+
for user_msg, assistant_msg in chat_history:
|
77 |
+
if user_msg:
|
78 |
+
messages.append({"role": "user", "content": user_msg})
|
79 |
+
if assistant_msg:
|
80 |
+
messages.append({"role": "assistant", "content": assistant_msg})
|
81 |
+
|
82 |
+
if user_message:
|
83 |
+
user_message += "/n (請一定要用繁體中文回答 zh-TW,並用台灣人的禮貌口語表達,回答時不要特別說明這是台灣人的語氣,不用提到「逐字稿」這個詞,用「內容」代替),回答時請用數學符號代替文字(Latex 用 $ 字號 render)"
|
84 |
+
messages.append({"role": "user", "content": user_message})
|
85 |
+
return messages
|
86 |
+
|
87 |
+
def chat_with_service(self, service_type, system_prompt, messages):
|
88 |
+
if service_type == 'jutor':
|
89 |
+
return self.chat_with_jutor(system_prompt, messages)
|
90 |
+
elif service_type == 'groq':
|
91 |
+
return self.chat_with_groq(system_prompt, messages)
|
92 |
+
elif service_type == 'claude3':
|
93 |
+
return self.chat_with_claude3(system_prompt, messages)
|
94 |
+
else:
|
95 |
+
raise gr.Error("不支持的服务类型")
|
96 |
+
|
97 |
+
def chat_with_jutor(self, system_prompt, messages):
|
98 |
+
messages.insert(0, {"role": "system", "content": system_prompt})
|
99 |
+
api_endpoint = "https://ci-live-feat-video-ai-dot-junyiacademy.appspot.com/api/v2/jutor/hf-chat"
|
100 |
+
headers = {
|
101 |
+
"Content-Type": "application/json",
|
102 |
+
"x-api-key": self.jutor_chat_key,
|
103 |
+
}
|
104 |
+
data = {
|
105 |
+
"data": {
|
106 |
+
"messages": messages,
|
107 |
+
"max_tokens": 512,
|
108 |
+
"temperature": 0.9,
|
109 |
+
"model": "gpt-4-1106-preview",
|
110 |
+
"stream": False,
|
111 |
+
}
|
112 |
+
}
|
113 |
+
|
114 |
+
response = requests.post(api_endpoint, headers=headers, data=json.dumps(data))
|
115 |
+
response_data = response.json()
|
116 |
+
response_completion = response_data['data']['choices'][0]['message']['content'].strip()
|
117 |
+
return response_completion
|
118 |
+
|
119 |
+
def chat_with_groq(self, system_prompt, messages):
|
120 |
+
# system_prompt insert to messages 的最前面 {"role": "system", "content": system_prompt}
|
121 |
+
messages.insert(0, {"role": "system", "content": system_prompt})
|
122 |
+
request_payload = {
|
123 |
+
"model": "mixtral-8x7b-32768",
|
124 |
+
"messages": messages,
|
125 |
+
"max_tokens": 1000 # 設定一個較大的值,可根據需要調整
|
126 |
+
}
|
127 |
+
groq_client = self.ai_client
|
128 |
+
response = groq_client.chat.completions.create(**request_payload)
|
129 |
+
response_completion = response.choices[0].message.content.strip()
|
130 |
+
return response_completion
|
131 |
+
|
132 |
+
def chat_with_claude3(self, system_prompt, messages):
|
133 |
+
if not system_prompt.strip():
|
134 |
+
raise ValueError("System prompt cannot be empty")
|
135 |
+
|
136 |
+
model_id = "anthropic.claude-3-sonnet-20240229-v1:0"
|
137 |
+
# model_id = "anthropic.claude-3-haiku-20240307-v1:0"
|
138 |
+
kwargs = {
|
139 |
+
"modelId": model_id,
|
140 |
+
"contentType": "application/json",
|
141 |
+
"accept": "application/json",
|
142 |
+
"body": json.dumps({
|
143 |
+
"anthropic_version": "bedrock-2023-05-31",
|
144 |
+
"max_tokens": 1000,
|
145 |
+
"system": system_prompt,
|
146 |
+
"messages": messages
|
147 |
+
})
|
148 |
+
}
|
149 |
+
print(messages)
|
150 |
+
# 建立 message API,讀取回應
|
151 |
+
bedrock_client = self.ai_client
|
152 |
+
response = bedrock_client.invoke_model(**kwargs)
|
153 |
+
response_body = json.loads(response.get('body').read())
|
154 |
+
response_completion = response_body.get('content')[0].get('text').strip()
|
155 |
+
return response_completion
|
educational_material.py
ADDED
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
|
3 |
+
class EducationalMaterial:
|
4 |
+
"""
|
5 |
+
A class to generate educational content based on provided parameters.
|
6 |
+
|
7 |
+
Attributes:
|
8 |
+
context (str): Context or transcript of the content.
|
9 |
+
topic (str): The topic of the content.
|
10 |
+
grade (str): The grade level for the content.
|
11 |
+
level (str): The difficulty level of the content.
|
12 |
+
specific_feature (str): A specific feature of the content.
|
13 |
+
content_type (str): The type of content to generate ('worksheet' or 'lesson_plan').
|
14 |
+
"""
|
15 |
+
def __init__(self, context, topic, grade, level, specific_feature, content_type):
|
16 |
+
"""
|
17 |
+
Initializes the EducationalMaterial with provided parameters.
|
18 |
+
|
19 |
+
Parameters:
|
20 |
+
context (str): Context or transcript of the content.
|
21 |
+
topic (str): The topic of the content.
|
22 |
+
grade (str): The grade level for the content.
|
23 |
+
level (str): The difficulty level of the content.
|
24 |
+
specific_feature (str): A specific feature of the content.
|
25 |
+
content_type (str): The type of content to generate ('worksheet' or 'lesson_plan').
|
26 |
+
"""
|
27 |
+
self.context = self._prepare_context(context)
|
28 |
+
self.topic = topic
|
29 |
+
self.grade = grade
|
30 |
+
self.level = level
|
31 |
+
self.specific_feature = specific_feature
|
32 |
+
self.content_type = content_type # 'worksheet' or 'lesson_plan'
|
33 |
+
self.system_content = "你是一個擅長資料分析跟影片教學備課的老師,請精讀資料文本,自行判斷資料的種類,使用 zh-TW"
|
34 |
+
|
35 |
+
def _prepare_context(self, context):
|
36 |
+
context_json = json.loads(context)
|
37 |
+
for entry in context_json:
|
38 |
+
entry.pop('embed_url', None)
|
39 |
+
entry.pop('screenshot_path', None)
|
40 |
+
processed_context = json.dumps(context_json, ensure_ascii=False, indent=2)
|
41 |
+
return processed_context
|
42 |
+
|
43 |
+
def generate_content_prompt(self):
|
44 |
+
if self.content_type == 'worksheet':
|
45 |
+
return self._generate_worksheet_prompt()
|
46 |
+
elif self.content_type == 'lesson_plan':
|
47 |
+
return self._generate_lesson_plan_prompt()
|
48 |
+
elif self.content_type == 'exit_ticket':
|
49 |
+
return self._generate_exit_ticket_prompt()
|
50 |
+
|
51 |
+
def _generate_worksheet_prompt(self):
|
52 |
+
bloom_worksheet_prompt = """
|
53 |
+
你是個專業的教師,熟悉布魯姆(Benjamin Bloom, 1964) 的認知理論。布魯姆認為人類的能力,大致可分為三個領域(domains),即認知領域(cognitive domain)、情意領域(affective domain)、技能領域 (psychomotor domain)。
|
54 |
+
|
55 |
+
【認知領域】涉及知能及其運作,著重心智、學習以及問題解決的工作。認知目標從簡單的認識或記憶能力到複雜的評鑑能力。大部分的教育目標都屬於這個領域。認知領域的目標分為六個層次,每個層次代表不同的心智功能。
|
56 |
+
- 📖 知識:在認知目標中知識是最低層次的能力,包括名詞、事實、規則和原理原則等的認識和記憶。用來表示此種能力的行為動詞有:指出、寫出、界定、說明、舉例、命名、認明等。例:能在地圖上指出長江流經的省分。
|
57 |
+
- 🤔 理解:理解是指能把握所學過知識或概念的意義,包含轉譯、解釋、推論等能力。代表此能力的行為動詞有:解釋、說明、區別、舉例、摘要、歸納等。例:能解釋光合作用。
|
58 |
+
- 🛠️ 應用:應用是指將所學到的規則、方法、步驟、原理、原則和概念,應用到新情境的能力。用來表示此能力的行為動詞有:預測、證明、解決、修改、表現、應用等。例:學生能預測抽出容器內之氣體對容器的影響。
|
59 |
+
- 🔍 分析:分析是指將所學到的概念或原則,分析為各個構成的部分,或找出各部分之間的相互關係,包括要素、關係及組織原理等的分析。用以表示此種能力的行為動詞有:選出、分析、判斷:區分、指出某些組成要素、指出某些的相互關係等。例:讀完某篇文章後,學生能區分事實和意見。
|
60 |
+
- 🌐 綜合:綜合是指將所學到的片斷概念或知識、原理原則或事實,統整成新的整體。用來表示此種能力的行為動詞有:設計、組織、綜合、創造、歸納、聯合等。例:讀完一篇有關防治汙染的文章後,學生能綜合防治汙染的各種方法。
|
61 |
+
- 🏅 評鑑:評鑑是認知目標中最高層次的能力,指依據某項標準做價值的判斷的能力。用來表示此能力的行為動詞有:評鑑、判斷、評論、比較、批判等。例:學生能評斷辯論中的謬論。
|
62 |
+
|
63 |
+
學習單包含以下的內容,將以布魯姆教育目標來建構提問的架構;請用 markdown 格式來呈現。
|
64 |
+
- 📝 主題:請使用上傳檔案的檔名作為標題
|
65 |
+
- 🔑 重點: 和影片有關重要知識摘要
|
66 |
+
- 💭 概念:概念性問題 - 布魯姆的知識層級;數學知識的建構
|
67 |
+
- 📊 想一想:程序性問題 - 布魯姆的理解層級;和影片相同的例題,類似的練習題 → 計算與步驟操作
|
68 |
+
- 🚀 延伸與應用 - 布魯姆的應用、分析、綜合、評鑑層級 → 延伸思考與應用
|
69 |
+
|
70 |
+
其中,「重點」的題目請用挖空的填空題;在「想一想」的程序性問題請以單選題或填空題的形式來建立,需要 3 個題目;「延伸與應用」請使用問答題的形式來建立,一題即可。
|
71 |
+
題目和題目之間要換行,並加上 point 符號,像是 "-" 或是 "1." 等等
|
72 |
+
|
73 |
+
這是範例格式:
|
74 |
+
🌐【主題】:認識公里
|
75 |
+
|
76 |
+
🎓【對象】
|
77 |
+
科目: 數學
|
78 |
+
年級: 三年級
|
79 |
+
難度: 基礎
|
80 |
+
|
81 |
+
🏞️【情境描述】
|
82 |
+
狐狸貓和家人出遊,過程中認識測量較長距離的單位「公里」。
|
83 |
+
|
84 |
+
🔑【影片重點】
|
85 |
+
- 公里是用來測量長距離的單位,通常用於測量很遠的距離。
|
86 |
+
- 1公里等於___公尺,也稱為千米。
|
87 |
+
- 公里的英文簡寫是 ____。
|
88 |
+
|
89 |
+
🌟【概念】
|
90 |
+
- 請問公里通常用於測量什麼類型的距離?
|
91 |
+
- 如果一圈操場是200公尺,那跑5圈是多少公尺?多少公里?
|
92 |
+
- 為什麼我們需要使用公里這個單位來測量距離?
|
93 |
+
|
94 |
+
🔢【想一想】
|
95 |
+
1. 一圈操場是200公尺,跑10圈是多少公里?(A) 1公里 (B) 2公里 (C) 3公里 (D) 4公里
|
96 |
+
2. 如果你跑了5圈操場,運動手環上會顯示你跑了多少公里?
|
97 |
+
3. 6000公尺等於多少公里?
|
98 |
+
|
99 |
+
💡【延伸與應用】
|
100 |
+
- 假設你參加一場慈善路跑活動,全程是5公里。如果你已經跑了3公里,還剩下多少公里?你覺得這樣的活動對你的體能有什麼影響?
|
101 |
+
"""
|
102 |
+
|
103 |
+
Polya_worksheet_prompt = """
|
104 |
+
你是個專業的教師,熟悉 George Polya(1945) 的數學問題解決策略。
|
105 |
+
Polya提出了一個四步驟的數學問題解決策略,在他影響深遠的經典著作 How to solve it《如何解題》中指出解難過程可分為四個階段:
|
106 |
+
(1) 理解問題 (understanding the problem)
|
107 |
+
(2) 設計解題策略 (devising a plan)
|
108 |
+
(3) 按步解題 (carrying out the plan)
|
109 |
+
(4) 回顧解答 (looking back) (edited)
|
110 |
+
|
111 |
+
請以此設計學習單並依據文本跟難度給予題目
|
112 |
+
請一定要使用 zh-TW
|
113 |
+
|
114 |
+
這是範例格式:
|
115 |
+
🌐 主題:【概念】認識公里
|
116 |
+
|
117 |
+
🎓【對象】
|
118 |
+
科目: 數學
|
119 |
+
年級: 三年級
|
120 |
+
難度: 基礎
|
121 |
+
|
122 |
+
🏞️【情境描述】
|
123 |
+
狐狸貓和家人出遊,過程中認識測量較長距離的單位「公里」。
|
124 |
+
|
125 |
+
❓【給出問題】
|
126 |
+
- 如果日月潭環潭自行車道共30公里,狐狸貓騎行了13.7公里後休息,剩餘多少公里尚未騎行?
|
127 |
+
|
128 |
+
🤔【理解問題】
|
129 |
+
首先,我們必須完全理解問題的所有細節。在此情境中,我們需要釐清以下幾點:
|
130 |
+
- 日月潭環潭自行車道的總長度為30公里。
|
131 |
+
- 狐狸貓已經騎行了13.7公里。
|
132 |
+
- 我們的目標是計算狐狸貓還剩下多少公里未騎行。
|
133 |
+
|
134 |
+
🧭【設計解題策略】
|
135 |
+
接下來,根據我們對問題的理解來設計解決問題的計劃。在此步驟中,我們決定採用哪種策略來解決問題。對於這個問題,最直接的計劃是使用算術減法:
|
136 |
+
- 總公里數(30公里)減去已騎行的公里數(13.7公里)。
|
137 |
+
|
138 |
+
📝【按步解題】
|
139 |
+
按照設計的計劃執行解題步驟。在這裡我們執行減法運算:
|
140 |
+
30−13.7=16.3
|
141 |
+
這意味著狐狸貓還有16.3公里未騎行。
|
142 |
+
|
143 |
+
✨【回顧解答】
|
144 |
+
最後,解題完成後,我們回顧和反思整個解題過程。在這一步,我們驗證「想一想」的結果是否符合邏輯,是否正確解決了原問題。
|
145 |
+
同時,考慮是否有更有效或簡便的方法來解決類似的問題。在這個例子中,使用直接減法是最簡單直接的方法。
|
146 |
+
但在其他情境下,可能需要考慮使用圖形、表格或代數表達式等其他策略來解決問題。
|
147 |
+
"""
|
148 |
+
|
149 |
+
# CRA教學法
|
150 |
+
cra_worksheet_prompt = """
|
151 |
+
你是個專業的教師,熟悉CRA教學法,CRA教學法是一種教學策略,
|
152 |
+
CRA正是一種用來促進學生學習和記憶數學的三步教學法,它闡明瞭用這種方式進行教學的具體步驟。
|
153 |
+
CRA的三個步驟互相依賴,運用CRA能建立起一種概念結構,從而形成知識的意義關聯。
|
154 |
+
CRA策略的第一個階段, 即實例(C)階段,是一個“做”的階段。在這一階段,教師用加工材料建模,這些材料包括彩色圓片、立方體、十進位積木、六形六色積木,以及分數積木,等等。在使用這些材料時,必須考慮到兒童的視覺、觸覺及動感經驗。
|
155 |
+
階段二,即描述(R)階段,是一個“看”的階段。在這一-階段,具體的模型被改成了圖片展示,教師可用手繪圖片或貼紙來對概念進行解釋。
|
156 |
+
最後一個階段,即抽象(A)階段,是一個抽象的“符號”階段, 在這一階段, 教師使用數字、字母等數學符號(如:2, 6, 3x, +,-等)來進行教學。
|
157 |
+
運用CRA的前提是學生在學習“規則”前必須學會概念。使用過加工材料的學生,其思維更加明確,更容易理解該方式,同時其學習動機、專注行爲、 理解力以及對這些概念的運用能力也會得到較大改善( Hrrison & Harison, 1986 )。
|
158 |
+
CRA策略可以有效地幫助學生理解以下幾個概念:早期數量關係、位值、計算、分數、小數、測量、幾何、貨幣、百分數、數基、應用題、概率以及統計等
|
159 |
+
這是範例格式:
|
160 |
+
🌐 主題:【概念】認識公里
|
161 |
+
|
162 |
+
🎓【對象】
|
163 |
+
科目: 數學
|
164 |
+
年級: 三年級
|
165 |
+
難度: 基礎
|
166 |
+
|
167 |
+
【實例(C)階段】
|
168 |
+
1. 用彩色圓片來解釋什麼是分數?
|
169 |
+
2. 用立方體來解釋什麼是體積?
|
170 |
+
3. 用十進位積木來解釋什麼是小數?
|
171 |
+
|
172 |
+
【描述(R)階段】
|
173 |
+
1. 用手繪圖片來解釋什麼是分數?
|
174 |
+
2. 用貼紙來解釋什麼是體積?
|
175 |
+
3. 用手繪圖片來解釋什麼是小數?
|
176 |
+
|
177 |
+
【抽象(A)階段】
|
178 |
+
1. 用數字來解釋什麼是分數?
|
179 |
+
2. 用字母來解釋什麼是體積?
|
180 |
+
3. 用數字來解釋什麼是小數?
|
181 |
+
|
182 |
+
"""
|
183 |
+
algorithm = self.specific_feature
|
184 |
+
case = {
|
185 |
+
"Bloom認知階層理論": bloom_worksheet_prompt,
|
186 |
+
"Polya數學解題法": Polya_worksheet_prompt,
|
187 |
+
"CRA教學法": cra_worksheet_prompt
|
188 |
+
}
|
189 |
+
worksheet_prompt = case.get(algorithm, "Bloom認知階層理論")
|
190 |
+
|
191 |
+
return worksheet_prompt
|
192 |
+
|
193 |
+
def _generate_lesson_plan_prompt(self):
|
194 |
+
lesson_plan_ADDIE_prompt = """
|
195 |
+
你是一位專業教師,
|
196 |
+
請根據以上要教學的項目細節(主題、年級、課程時間、課程目標)
|
197 |
+
幫我安排一個 lesson plan
|
198 |
+
|
199 |
+
規則如下,請嚴格遵守
|
200 |
+
1. 請使用繁體中文溝通 zh-TW,並使用台灣人的用語
|
201 |
+
2. 該換行就換行,盡量滿足 word .doc 的格式
|
202 |
+
3. 【課程大綱】的工作項目請嚴格遵守【課程時間】的時間長度,總和時間不要超過或不足
|
203 |
+
4. 時間安排盡量以五分鐘的倍數為一個單位
|
204 |
+
5. 並且根據課程目標安排教學內容
|
205 |
+
Step1. 你是一個精通於課程設計的老師,能夠運用 ADDIE 教學策略來設計一門適合學生的課程。
|
206 |
+
Step2. 接下來,當使用者按下 "Conversation staters" 的 "請上傳逐字稿",你將依據輸入的影片逐字稿 (請使用 python 程式來分析、解讀內容),來規劃一堂 40 分鐘以 ADDIE 模型作為架構的課程,內容請使用台灣口語 (zh_TW)。
|
207 |
+
- 以下是 ADDIE 的說明:
|
208 |
+
|
209 |
+
ADDIE 教學設計模式是一種廣泛應用於教育和訓練領域的系統化教學設計流程。ADDIE 代表分析(Analysis)、設計(Design)、開發(Development)、實施(Implementation)、評估(Evaluation)五個階段:
|
210 |
+
1. 分析(Analysis)階段:在這一階段,設計者需評估教學需求和學習者的背景,包括學習者的知識水平、學習風格及教學目標等。
|
211 |
+
2. 設計(Design)階段:基於分析階段的資料,開始構思教學計劃,包括課程目標、教學策略、內容架構、評估方法等。
|
212 |
+
a. 轉化課程目標為:單元目標和表現的結果
|
213 |
+
b. 決定可以涵括這些目標的主題或單元,計算實際實施教學時會花費的時間
|
214 |
+
c. 依課程目標安排課程順序
|
215 |
+
d. 設計教學的單元,並確認在這些單元中所要達到的主要目標
|
216 |
+
e. 規劃各單元的學習活動
|
217 |
+
f. 發展特定的評量以確認學生的學習情況
|
218 |
+
3. 開發(Development)階段:這個階段主要是製作教學材料,如課程內容、學習活動、多媒體素材等。
|
219 |
+
4. 實施(Implementation)階段:在此階段,實際執行教學計劃,這包括教授課程、指導學習者及管理學習過程。
|
220 |
+
|
221 |
+
5 評估(Evaluation)階段:評估整個教學設計和實施的有效性,這不僅包括學習成果的評估,還要檢視整個教學過程的有效性與可改進之處。
|
222 |
+
a. 教材:是否達到教學目標?
|
223 |
+
b. 過程:設計過程中的品質是否OK?
|
224 |
+
c. 學習者的反應/感受
|
225 |
+
d. 學習者在學習後達成之成就
|
226 |
+
e. 教學實施的結果
|
227 |
+
|
228 |
+
Step3. 請在描述課程計畫的一開始,條列此影片逐字稿的教學重點 (必須和學科領域相關,例如:數學、自然科學) 後,再依序建立以 ADDIE 框架的課程計畫。
|
229 |
+
Step4. 請在「設計」和「實施」階段請使用合適的範例做說明。
|
230 |
+
Step5. 請使用 Markdown 語法,標題與段落文字的字級需有所區隔。
|
231 |
+
|
232 |
+
example:
|
233 |
+
# 課程設計:面積的實測與估測
|
234 |
+
|
235 |
+
# 教學重點
|
236 |
+
1. 面積的概念:解釋什麼是面積,以及它在日常生活中的應用。
|
237 |
+
2. 面積的計算:教授如何計算不同形狀的面積(例如矩形、三角形)。
|
238 |
+
3. 實測與估測的區別:比較實際測量面積和估算面積的不同方法。
|
239 |
+
4. 應用實例:使用日常生活中的例子,例如給麥麥的生日卡片大小的比較,來說明面積的重要性。
|
240 |
+
|
241 |
+
# 分析(Analysis)
|
242 |
+
- 目標學習者:小學高年級學生。
|
243 |
+
- 學習需求:理解面積概念,掌握基本面積計算方法。
|
244 |
+
- 學習風格:結合視覺教材和互動活動。
|
245 |
+
|
246 |
+
# 設計(Design)
|
247 |
+
- 課程目標:學生能夠理解面積概念,並能透過實際操作學會估算和計算面積。
|
248 |
+
- 學習單元:(a)面積基礎概念(b)計算面積(c)實測與估測的比較。
|
249 |
+
- 教學策略:透過互動遊戲、案例討論和實作活動進行教學。
|
250 |
+
- 評估方法:透過小測驗和課堂參與情況進行評估。
|
251 |
+
|
252 |
+
# 開發(Development)
|
253 |
+
- 教材準備:準備視覺化教學幻燈片,設計互動遊戲和實作活動。
|
254 |
+
- 教學活動:設計面積估測遊戲,讓學生在實際操作中學習。
|
255 |
+
|
256 |
+
# 實施(Implementation)
|
257 |
+
- 課程時間分配:(a)面積基礎概念(10分鐘)(b)計算面積(15分鐘)(c)實測與估測比較(10分鐘)(d)總結與回顧(5分鐘)。
|
258 |
+
- 教學範例:
|
259 |
+
- 面積基礎概念:介紹面積是如何在日常生活中應用,例如計算桌布的大小。
|
260 |
+
- 計算面積:透過計算教室黑板或門的面積來教學。
|
261 |
+
- 實測與估測比較:讓學生估計並實際測量教室中物品的面積,比較結果。
|
262 |
+
|
263 |
+
# 評估(Evaluation)
|
264 |
+
- 教材評估:確保教材能清楚傳達面積概念。
|
265 |
+
- 學習者反應:觀察學生參與情況,收集反饋以改善教學。
|
266 |
+
- 學習成就:透過小測驗評估學生對面積概念的理解。
|
267 |
+
- 教學實施結果:評估課程的整體有效性,並根據學生的學習成果進行調整。
|
268 |
+
|
269 |
+
這個課程計畫將幫助學生透過互動和實作活動來深入理解面積的概念和計算方法
|
270 |
+
"""
|
271 |
+
|
272 |
+
lesson_plan_5E_prompt = """
|
273 |
+
你是一位專業教師,
|
274 |
+
請根據以上要教學的項目細節(主題、年級、課程時間、課程目標)
|
275 |
+
幫我安排一個 lesson plan
|
276 |
+
|
277 |
+
規則如下,請嚴格遵守
|
278 |
+
1. 請使用繁體中文溝通 zh-TW,並使用台灣人的用語
|
279 |
+
2. 該換行就換行,盡量滿足 word .doc 的格式
|
280 |
+
3. 【課程大綱】的工作項目請嚴格遵守【課程時間】的時間長度,總和時間不要超過或不足
|
281 |
+
4. 時間安排盡量以五分鐘的倍數為一個單位
|
282 |
+
5. 並且根據課程目標安排教學內容
|
283 |
+
|
284 |
+
Lesson Planner 5E is specialized in developing lesson plans for various subjects including Mathematics, Physics, Chemistry, and Biology, using the 5E instructional model. Teachers can input transcripts or text related to these subjects, and the GPT will analyze the content to create a comprehensive lesson plan. The plan will follow the 5E stages: Engagement, Exploration, Explanation, Elaboration, and Evaluation. Each stage will be customized to suit the subject matter, ensuring that students are engaged and the learning objectives are met. For Mathematics, the GPT might suggest problem-solving activities; for Physics, interactive experiments; for Chemistry, lab demonstrations; and for Biology, field studies or observations. The GPT's goal is to make science and math subjects accessible and interesting for students, fostering a deeper understanding through hands-on and inquiry-based learning.
|
285 |
+
|
286 |
+
Step1:理解什麼是 5E 教學模式,請閱讀以下文字:
|
287 |
+
5E教學模式:
|
288 |
+
由Trowbridge和Bybee (1990)提出以探究為基礎的5E教學模式,將教學過程劃分為五個緊密相連的階段,包括:
|
289 |
+
參與(engagement)、探索(exploration)、解釋(explanation)、精緻化(elaboration)與評量(evaluation)等五個階段,
|
290 |
+
各階段的內容如下:
|
291 |
+
1.參與:以學生的學習為主體,設計活動引發學生的學習興趣,使學生願意主動參與教學活動,並能將學生的舊經驗與課程內容相連結,經由提問、定義問題與呈現矛盾的結果等方式,引出探討主題的方向。
|
292 |
+
2.探索:學生參與活動,並給予足夠時間與機會進行探索任務,經由動手操作,建構共同的、具體的經驗。
|
293 |
+
3.解釋:先請學生提出解釋,教師再以學生的想法為基礎,並運用口頭、影片或教學媒體等方式,對學生的解釋加以闡述確認,使學生能確實理解學科知識,再引導學生進入下一階段的教學流程。
|
294 |
+
4.學習共同體 (精緻化):重視學生之間的互動,營造能促使學生討論以及互相合作的學習環境,分享想法並給予回饋,以建構個人的理解。此外,此階段亦重視學生是否能將其所形成的解釋,應用於新的情境或問題中,以延伸更加一般化的概念理解,進而獲取高層次的知識。
|
295 |
+
5.評量:此階段的主要目的為鼓勵學生評估自己的理解力與能力,同時老師也藉由評量確認學生是否達成教學目標該有的程度。在學生進行探索與提出解釋後,給予回饋是相當重要的,因而教師在階段活動後應進行形成性評量
|
296 |
+
|
297 |
+
Step2:請根據逐字稿的內容描述,以 5E 架構來創建一份 Lesson Plan (課程計畫)。請依循並參考以下輸出範例來設計課程計畫),需使用 markdown 語法:
|
298 |
+
|
299 |
+
## 輸出範例:
|
300 |
+
|
301 |
+
課程主題:同分母分數的加減 (使用影片或逐字稿的標題)
|
302 |
+
課程重點:(列出逐字稿內容的重要概念或教學重點)
|
303 |
+
年級:四年級 (依據詢問的年齡或年級填入)
|
304 |
+
課程長度:45-50 分鐘(根據課程數入時間訂定)
|
305 |
+
|
306 |
+
【參與】(5 分鐘)
|
307 |
+
|
308 |
+
- 透過在黑板或投影機上顯示切成不同部分的披薩的圖片來開始課程。
|
309 |
+
- 讓學生與夥伴討論他們注意到分數的什麼以及如何將它們加在一起或減在一起。
|
310 |
+
- 幾分鐘後,請幾位學生與全班分享他們的觀察和想法。
|
311 |
+
|
312 |
+
【探索】(10 分鐘)
|
313 |
+
|
314 |
+
- 發給每位學生分數操作工具(例如分數條、分數圓)。
|
315 |
+
- 為每個學生提供一份工作表,其中包含幾個涉及具有相同分母的分數的加法和減法問題。
|
316 |
+
- 指導學生使用操作工具解決工作表上的問題。
|
317 |
+
- 在房間裡走動,觀察學生的理解情況,並根據需要提供指導。
|
318 |
+
|
319 |
+
【解釋】(10 分鐘)
|
320 |
+
|
321 |
+
- 讓全班同學重新聚集在一起,討論相同分母分數加法和減法的策略和方法。
|
322 |
+
- 介紹關鍵詞彙:分子、分母、分數、同分母。
|
323 |
+
- 顯示並解釋公式:如果兩個分數具有相同的分母,則將分子相加或相減,然後寫出公分母的和或差。
|
324 |
+
- 提供同分母分數加減法的例子,示範步驟和計算。
|
325 |
+
|
326 |
+
【學習共同體】(15 分鐘)
|
327 |
+
|
328 |
+
- 將學生分成兩人一組或小組。
|
329 |
+
- 向每組分發一組分數加法和減法任務卡。
|
330 |
+
- 指導學生使用前面討論的方法一起解決任務卡上的問題。
|
331 |
+
- 鼓勵學生互相解釋他們的想法和推理。
|
332 |
+
- 在教室裡走動以了解學生的進度並在需要時提供支持或澄清。
|
333 |
+
|
334 |
+
【評估】(10 分鐘)
|
335 |
+
|
336 |
+
- 要求每個小組選擇一張任務卡並向全班展示他們的解決方案。
|
337 |
+
- 作為一個班級,討論各小組使用的不同策略並評估他們解決方案的正確性。
|
338 |
+
- 提供回饋並澄清討論期間出現的任何誤解。
|
339 |
+
- 收集學生的工作表和任務卡,作為他們理解的形成性評估。
|
340 |
+
|
341 |
+
【總結】
|
342 |
+
|
343 |
+
- 總結課程中討論的重點,強調分數加減法時分母相同的重要性。
|
344 |
+
- 請學生反思他們的學習,並寫一兩句話來說明他們認為今天的課程有挑戰性或有趣的地方。
|
345 |
+
|
346 |
+
【學習評估】
|
347 |
+
|
348 |
+
- 形成性評估:探索和細化階段的觀察以及討論和演示將深入了解學生對相同分母分數加法和減法的理解。
|
349 |
+
- 總結性評估:收集並審查學生的工作表和任務卡,以評估他們解決涉及具有相同分母的分數的加法和減法問題的能力。
|
350 |
+
"""
|
351 |
+
lesson_plan_prompt = lesson_plan_ADDIE_prompt
|
352 |
+
|
353 |
+
return lesson_plan_prompt
|
354 |
+
|
355 |
+
def _generate_exit_ticket_prompt(self):
|
356 |
+
exit_ticket_prompt = """
|
357 |
+
你是一位專業教師,
|
358 |
+
請根據���上要教學的項目細節及逐字稿
|
359 |
+
在一節課結束前用上述的時間(10分鐘內)
|
360 |
+
運用出場券 (exit ticket) 來檢核學生的學習是否有對上學習目標,有哪些知識點或技能還不熟悉
|
361 |
+
目的是讓教學更加精準;出場券能夠檢視每一堂課的學習成效,從學生的答題狀況,
|
362 |
+
可以觀察班上有多少比例學生沒學會,沒學會的地方又是什麼。
|
363 |
+
|
364 |
+
規則如下,請嚴格遵守:
|
365 |
+
1. 內容請使用台灣口語的繁體中文 (zh_TW),不可以使用大陸用語,也不可以使用簡體字。
|
366 |
+
2. 該換行就換行,盡量滿足 word .doc 的格式
|
367 |
+
3. 規劃限定時間內,學生能夠完成的出場券習題,共有 5 題,請使用 3 個單選題 (四選項) 以及 2 個填空題,題目必須能夠符合此逐字稿內容描述的數學學習目標,以類題方式建立。
|
368 |
+
4. 此出場券習題最末請公布各題的答案。
|
369 |
+
|
370 |
+
example:
|
371 |
+
出場券習題:公里、公尺和公分的換算
|
372 |
+
選擇題(請從四個選項中選出一個正確答案)
|
373 |
+
|
374 |
+
1. 1公里等於多少公尺?
|
375 |
+
A) 100公尺
|
376 |
+
B) 1000公尺
|
377 |
+
C) 10,000公尺
|
378 |
+
D) 100公分
|
379 |
+
|
380 |
+
2. 5公尺等於多少公分?
|
381 |
+
A) 50公分
|
382 |
+
B) 500公分
|
383 |
+
C) 5,000公分
|
384 |
+
D) 50,000公分
|
385 |
+
|
386 |
+
3. 如果你走了2.5公里的路程,這段路程等於多少公尺?
|
387 |
+
A) 250公尺
|
388 |
+
B) 2,500公尺
|
389 |
+
C) 25,000公尺
|
390 |
+
D) 250,000公尺
|
391 |
+
|
392 |
+
填空題
|
393 |
+
1. 100公分等於___公尺?(請填入數字)
|
394 |
+
2. 1公尺等於___公分?(請填入數字)
|
395 |
+
|
396 |
+
|
397 |
+
答案公布
|
398 |
+
答案:B) 1000公尺
|
399 |
+
答案:B) 500公分
|
400 |
+
答案:B) 2,500公尺
|
401 |
+
答案:A) 3公尺
|
402 |
+
答案:1公尺
|
403 |
+
|
404 |
+
這套出場券習題旨在幫助學生掌握公里、公尺、和公分之間的換算關係,通過實際的例子加深學生對這些單位之間轉換的理解。
|
405 |
+
"""
|
406 |
+
return exit_ticket_prompt
|
407 |
+
|
408 |
+
def create_ai_content(self, ai_client, request_payload):
|
409 |
+
user_content = self.build_user_content()
|
410 |
+
messages = self.build_messages(user_content)
|
411 |
+
request_payload['messages'] = messages
|
412 |
+
response_content = self.send_ai_request(ai_client, request_payload)
|
413 |
+
|
414 |
+
return response_content
|
415 |
+
|
416 |
+
def build_user_content(self):
|
417 |
+
if self.content_type == 'worksheet':
|
418 |
+
specific_feature_text = f"理論模型: {self.specific_feature}"
|
419 |
+
elif self.content_type == 'lesson_plan':
|
420 |
+
specific_feature_text = f"時間: {self.specific_feature} 分鐘"
|
421 |
+
elif self.content_type == 'exit_ticket':
|
422 |
+
specific_feature_text = f"時間: {self.specific_feature} 分鐘"
|
423 |
+
|
424 |
+
# 根据属性构建用户内容
|
425 |
+
user_content = f"""
|
426 |
+
課程脈絡 or 逐字稿:{self.context}
|
427 |
+
主題:{self.topic}
|
428 |
+
年級:{self.grade}
|
429 |
+
難度:{self.level}
|
430 |
+
{specific_feature_text}
|
431 |
+
|
432 |
+
請根據逐字稿進行以下工作:
|
433 |
+
不要提到 【逐字稿】 這個詞,直接給出工作內容即可
|
434 |
+
如果是中文素材,請嚴格使用 zh-TW
|
435 |
+
請用 {self.grade} 年級的口吻,不要用太難的詞彙
|
436 |
+
{self.generate_content_prompt()}
|
437 |
+
"""
|
438 |
+
print("====User content====")
|
439 |
+
print(user_content)
|
440 |
+
print("====User content====")
|
441 |
+
return user_content
|
442 |
+
|
443 |
+
def build_messages(self, user_content):
|
444 |
+
messages = [{"role": "system", "content": self.system_content},
|
445 |
+
{"role": "user", "content": user_content}]
|
446 |
+
return messages
|
447 |
+
|
448 |
+
def send_ai_request(self, ai_client, request_payload):
|
449 |
+
try:
|
450 |
+
response = ai_client.chat.completions.create(**request_payload)
|
451 |
+
response_content = response.choices[0].message.content.strip()
|
452 |
+
return response_content
|
453 |
+
except Exception as e:
|
454 |
+
print(f"An error occurred: {e}")
|
455 |
+
return "Error generating content."
|
456 |
+
|
457 |
+
def build_fine_tune_user_content(self, original_prompt, result, fine_tune_prompt):
|
458 |
+
user_content = f"""
|
459 |
+
這是逐字稿:{self.context}
|
460 |
+
---
|
461 |
+
這是預設的 prompt
|
462 |
+
{original_prompt}
|
463 |
+
---
|
464 |
+
產生了以下的結果:
|
465 |
+
{result}
|
466 |
+
---
|
467 |
+
但我不是很滿意,請根據以下的調整,產生新的結果
|
468 |
+
{fine_tune_prompt}
|
469 |
+
"""
|
470 |
+
return user_content
|
local_config_example.json
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"OUTPUT_PATH": "/Users/young/Downloads",
|
3 |
+
"TRANSCRIPTS": [],
|
4 |
+
"CURRENT_INDEX": 0,
|
5 |
+
"VIDEO_ID": "",
|
6 |
+
"PASSWORD": "1234",
|
7 |
+
"OPEN_AI_KEY": "sk-",
|
8 |
+
"GROQ_API_KEY": "",
|
9 |
+
"JUTOR_CHAT_KEY": "",
|
10 |
+
"GOOGLE_APPLICATION_CREDENTIALS_JSON": {
|
11 |
+
}
|
12 |
+
}
|
requirements.txt
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
gradio
|
2 |
pandas
|
3 |
openai>=1.0.0
|
4 |
requests
|
@@ -12,4 +12,9 @@ google-api-python-client
|
|
12 |
google-auth-httplib2
|
13 |
google-auth-oauthlib
|
14 |
google-cloud-storage
|
15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio>=4.24.0
|
2 |
pandas
|
3 |
openai>=1.0.0
|
4 |
requests
|
|
|
12 |
google-auth-httplib2
|
13 |
google-auth-oauthlib
|
14 |
google-cloud-storage
|
15 |
+
groq
|
16 |
+
yt_dlp
|
17 |
+
uuid
|
18 |
+
gtts
|
19 |
+
boto3
|
20 |
+
pydub
|
storage_service.py
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
from google.cloud import storage
|
3 |
+
from google.oauth2 import service_account
|
4 |
+
from googleapiclient.http import MediaIoBaseDownload
|
5 |
+
|
6 |
+
|
7 |
+
class GoogleCloudStorage:
|
8 |
+
def __init__(self, service_account_key_string):
|
9 |
+
credentials_dict = json.loads(service_account_key_string)
|
10 |
+
credentials = service_account.Credentials.from_service_account_info(credentials_dict)
|
11 |
+
self.client = storage.Client(credentials=credentials, project=credentials_dict['project_id'])
|
12 |
+
|
13 |
+
def check_file_exists(self, bucket_name, file_name):
|
14 |
+
blob = self.client.bucket(bucket_name).blob(file_name)
|
15 |
+
return blob.exists()
|
16 |
+
|
17 |
+
def upload_file(self, bucket_name, destination_blob_name, file_path):
|
18 |
+
blob = self.client.bucket(bucket_name).blob(destination_blob_name)
|
19 |
+
blob.upload_from_filename(file_path)
|
20 |
+
print(f"File {file_path} uploaded to {destination_blob_name} in GCS.")
|
21 |
+
|
22 |
+
def upload_file_as_string(self, bucket_name, destination_blob_name, content):
|
23 |
+
blob = self.client.bucket(bucket_name).blob(destination_blob_name)
|
24 |
+
blob.upload_from_string(content)
|
25 |
+
print(f"String content uploaded to {destination_blob_name} in GCS.")
|
26 |
+
return None
|
27 |
+
|
28 |
+
def download_as_string(self, bucket_name, source_blob_name):
|
29 |
+
blob = self.client.bucket(bucket_name).blob(source_blob_name)
|
30 |
+
return blob.download_as_text()
|
31 |
+
|
32 |
+
def make_blob_public(self, bucket_name, blob_name):
|
33 |
+
blob = self.client.bucket(bucket_name).blob(blob_name)
|
34 |
+
blob.make_public()
|
35 |
+
print(f"Blob {blob_name} is now publicly accessible at {blob.public_url}")
|
36 |
+
|
37 |
+
def get_public_url(self, bucket_name, blob_name):
|
38 |
+
blob = self.client.bucket(bucket_name).blob(blob_name)
|
39 |
+
return blob.public_url
|
40 |
+
|
41 |
+
def upload_image_and_get_public_url(self, bucket_name, file_name, file_path):
|
42 |
+
self.upload_file(bucket_name, file_name, file_path)
|
43 |
+
self.make_blob_public(bucket_name, file_name)
|
44 |
+
return self.get_public_url(bucket_name, file_name)
|