youngtsai commited on
Commit
3bdba8e
1 Parent(s): 5c4e35b
Files changed (8) hide show
  1. .gitignore +1 -0
  2. README.md +1 -0
  3. app.py +1385 -429
  4. chatbot.py +155 -0
  5. educational_material.py +470 -0
  6. local_config_example.json +12 -0
  7. requirements.txt +7 -2
  8. 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 json
 
 
 
 
 
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 io
26
- import time
27
-
28
-
29
- from urllib.parse import urlparse, parse_qs
30
-
31
-
32
- # 假设您的环境变量或Secret的名称是GOOGLE_APPLICATION_CREDENTIALS_JSON
33
- # credentials_json_string = os.getenv("GOOGLE_APPLICATION_CREDENTIALS_JSON")
34
- # credentials_dict = json.loads(credentials_json_string)
35
- # SCOPES = ['https://www.googleapis.com/auth/drive']
36
- # credentials = service_account.Credentials.from_service_account_info(
37
- # credentials_dict, scopes=SCOPES)
38
- # service = build('drive', 'v3', credentials=credentials)
39
- # # 列出 Google Drive 上的前10個文件
40
- # results = service.files().list(pageSize=10, fields="nextPageToken, files(id, name)").execute()
41
- # items = results.get('files', [])
42
-
43
- # if not items:
44
- # print('No files found.')
45
- # else:
46
- # print("=====Google Drive 上的前10個文件=====")
47
- # print('Files:')
48
- # for item in items:
49
- # print(u'{0} ({1})'.format(item['name'], item['id']))
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
- DRIVE_KEY = os.getenv("GOOGLE_APPLICATION_CREDENTIALS_JSON")
61
- GCS_KEY = os.getenv("GOOGLE_APPLICATION_CREDENTIALS_JSON")
62
-
63
- # ====gcs====
64
- def init_gcs_client(service_account_key_string):
65
- """使用服务账号密钥文件创建 GCS 客户端"""
66
- credentials_json_string = service_account_key_string
67
- credentials_dict = json.loads(credentials_json_string)
68
- credentials = service_account.Credentials.from_service_account_info(credentials_dict)
69
- gcs_client = storage.Client(credentials=credentials, project=credentials_dict['project_id'])
70
- return gcs_client
71
-
72
- def gcs_create_bucket_folder_if_not_exists(gcs_client, bucket_name, folder_name):
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
- print(f"GCS Folder '{folder_name}' already exists.")
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
- # ---- Main Functions ----
296
- def process_file(file):
 
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 = init_gcs_client(GCS_KEY)
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 = gcs_check_file_exists(gcs_client, bucket_name, transcript_blob_name)
437
  if not is_transcript_exists:
438
  # 从YouTube获取逐字稿并上传
439
- transcript = get_transcript(video_id)
 
 
 
 
 
 
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
- # for entry in transcript:
461
- # if 'img_file_id' not in entry:
462
- # screenshot_path = screenshot_youtube_video(video_id, entry['start'])
463
- # screenshot_blob_name = f"{video_id}/{video_id}_{entry['start']}.jpg"
464
- # img_file_id = upload_img_and_get_public_url(gcs_client, bucket_name, screenshot_blob_name, screenshot_path)
465
- # entry['img_file_id'] = img_file_id
466
- # print(f"截图已上传到GCS: {img_file_id}")
 
 
 
 
 
 
 
 
 
 
 
 
 
467
 
468
  # 更新逐字稿文件
469
- # print("===更新逐字稿文件===")
470
- # print(transcript)
471
- # print("===更新逐字稿文件===")
472
- # updated_transcript_text = json.dumps(transcript, ensure_ascii=False, indent=2)
473
- # upload_file_to_gcs_with_json_string(gcs_client, bucket_name, transcript_blob_name, updated_transcript_text)
474
- # print("逐字稿已更新,包括截图链接")
475
- # updated_transcript_json = json.loads(updated_transcript_text)
476
-
477
- # return updated_transcript_json
478
- return transcript
 
 
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
- # img_file_id = entry['img_file_id']
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
- # first_image = formatted_transcript[0]['screenshot_path']
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 = init_gcs_client(GCS_KEY)
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 = gcs_check_file_exists(gcs_client, bucket_name, blob_name)
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 = init_gcs_client(GCS_KEY)
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 = gcs_check_file_exists(gcs_client, bucket_name, summary_file_blob_name)
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 = init_gcs_client(GCS_KEY)
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 = gcs_check_file_exists(gcs_client, bucket_name, blob_name)
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 change_questions(df_string):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- def respond(user_message, data, chat_history, socratic_mode=False):
887
- print("=== 變數:user_message ===")
888
- print(user_message)
889
- print("=== 變數:chat_history ===")
890
- print(chat_history)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
891
 
892
- data_json = json.loads(data)
893
- for entry in data_json:
894
- entry.pop('embed_url', None) # Remove 'embed_url' if it exists
895
- entry.pop('screenshot_path', None)
 
 
896
 
897
- if socratic_mode:
898
- sys_content = f"""
899
- 你是一個擅長資料分析跟影片教學的老師,user 為學生
900
- 請用 {data} 為資料文本,自行判斷資料的種類,
901
- 並進行對話,使用 台灣人的口與表達,及繁體中文zh-TW
 
 
 
 
 
 
 
 
 
 
902
 
903
- 如果是影片類型,不用解釋逐字稿格式,直接回答學生問題
904
- 請你用蘇格拉底式的提問方式,引導學生思考,並且給予學生一些提示
905
- 不要直接給予答案,讓學生自己思考
906
- 但可以給予一些提示跟引導,例如給予影片的時間軸,讓學生自己去找答案
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
 
908
- 如果學生問了一些問題你無法判斷,請告訴學生你無法判斷,並建議學生可以問其他問題
909
- 或者你可以問學生一些問題,幫助學生更好的理解資料
 
 
 
 
910
 
911
- 如果學生的問題與資料文本無關,請告訴學生你無法回答超出範圍的問題
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
912
 
913
- 最後,在你回答的開頭標註【蘇格拉底助教】
914
- """
915
- else:
916
- sys_content = f"""
917
- 你是一個擅長資料分析跟影片教學的老師,user 為學生
918
- 請用 {data} 為資料文本,自行判斷資料的種類,
919
- 並進行對話,使用 zh-TW
920
 
921
- 如果是影片類型,不用解釋��字稿格式,直接回答學生問題
922
- 但可以給予一些提示跟引導,例如給予影片的時間軸,讓學生可以找到相對應的時間點
 
 
923
 
924
- 如果學生問了一些問題你無法判斷,請告訴學生你無法判斷,並建議學生可以問其他問題
925
- 或者你可以問學生一些問題,幫助學生更好的理解資料
926
 
927
- 如果學生的問題與資料文本無關,請告訴學生你無法回答超出範圍的問題
928
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
929
 
930
- messages = [
931
- {"role": "system", "content": sys_content}
932
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
933
 
934
- # if chat_history is not none, append role, content to messages
935
- # chat_history = [(user, assistant), (user, assistant), ...]
936
- # In the list, first one is user, then assistant
937
- if chat_history is not None:
938
- # 如果超過10則訊息,只保留最後10則訊息
939
- if len(chat_history) > 10:
940
- chat_history = chat_history[-10:]
941
-
942
- for chat in chat_history:
943
- old_messages = [
944
- {"role": "user", "content": chat[0]},
945
- {"role": "assistant", "content": chat[1]}
946
- ]
947
- messages += old_messages
948
- else:
949
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
950
 
951
- messages.append({"role": "user", "content": user_message})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
952
  request_payload = {
953
- "model": "gpt-4-1106-preview",
954
  "messages": messages,
955
- "max_tokens": 4000 # 設定一個較大的值,可根據需要調整
956
  }
957
- response = OPEN_AI_CLIENT.chat.completions.create(**request_payload)
958
- response_text = response.choices[0].message.content.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
959
 
960
- # 更新聊天历史
961
- new_chat_history = (user_message, response_text)
962
- if chat_history is None:
963
- chat_history = [new_chat_history]
964
- else:
965
- chat_history.append(new_chat_history)
 
 
 
 
 
 
 
 
 
 
 
 
966
 
967
- # 返回聊天历史和空字符串清空输入框
968
- return "", chat_history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- assistant_id = "asst_kmvZLNkDUYaNkMNtZEAYxyPq"
977
- client = OPEN_AI_CLIENT
978
-
979
- # 從 file 拿逐字稿資料
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
- if not thread_id:
1033
- thread = client.beta.threads.create()
1034
- thread_id = thread.id
1035
- else:
1036
- thread = client.beta.threads.retrieve(thread_id)
1037
-
1038
- # 向线程添加用户的消息
1039
- client.beta.threads.messages.create(
1040
- thread_id=thread.id,
1041
- role="user",
1042
- content=user_message
1043
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1044
 
1045
- # 运行助手,生成响应
1046
- run = client.beta.threads.runs.create(
1047
- thread_id=thread.id,
1048
- assistant_id=assistant_id,
1049
- instructions=instructions,
1050
- )
 
 
 
 
 
 
 
1051
 
1052
- # 等待助手响应,设定最大等待时间为 30 秒
1053
- run_status = poll_run_status(run.id, thread.id, timeout=30)
1054
- # 获取助手的响应消息
1055
- if run_status == "completed":
1056
- messages = client.beta.threads.messages.list(thread_id=thread.id)
1057
- # [MessageContentText(text=Text(annotations=[], value='您好!有什麼我可以幫助您的嗎?如果有任何問題或需要指導,請隨時告訴我!'), type='text')]
1058
- response_text = messages.data[0].content[0].text.value
1059
- else:
1060
- response_text = "學習精靈有點累,請稍後再試!"
1061
 
1062
- # 更新聊天历史
1063
- new_chat_history = (user_message, response_text)
1064
- if chat_history is None:
1065
- chat_history = [new_chat_history]
1066
- else:
1067
- chat_history.append(new_chat_history)
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- with gr.Column(scale=2):
1181
- file_upload = gr.File(label="Upload your CSV or Word file", visible=False)
1182
- youtube_link = gr.Textbox(label="Enter YouTube Link")
1183
- video_id = gr.Textbox(label="video_id", visible=False)
1184
- youtube_link_btn = gr.Button("Submit_YouTube_Link")
1185
- web_link = gr.Textbox(label="Enter Web Page Link", visible=False)
1186
- chatbot = gr.Chatbot()
1187
- thread_id = gr.Textbox(label="thread_id", visible=False)
1188
- socratic_mode_btn = gr.Checkbox(label="蘇格拉底家教助理模式", value=False)
1189
- msg = gr.Textbox(label="Message")
1190
- send_button = gr.Button("Send")
1191
-
1192
- with gr.Column(scale=3):
1193
- with gr.Tab("圖文"):
1194
- transcript_html = gr.HTML(label="YouTube Transcript and Video")
1195
- with gr.Tab("投影片"):
1196
- slide_image = gr.Image()
1197
- slide_text = gr.Textbox()
1198
  with gr.Row():
1199
- prev_button = gr.Button("Previous")
1200
- next_button = gr.Button("Next")
1201
- prev_button.click(fn=prev_slide, inputs=[], outputs=[slide_image, slide_text])
1202
- next_button.click(fn=next_slide, inputs=[], outputs=[slide_image, slide_text])
1203
- with gr.Tab("逐字稿"):
1204
- simple_html_content = gr.HTML(label="Simple Transcript")
1205
- with gr.Tab("本文"):
1206
- df_string_output = gr.Textbox(lines=40, label="Data Text")
1207
- with gr.Tab("重點"):
1208
- df_summarise = gr.Textbox(container=True, show_copy_button=True, lines=40)
1209
- with gr.Tab("問題"):
1210
- gr.Markdown("## 常用問題")
1211
- btn_1 = gr.Button()
1212
- btn_2 = gr.Button()
1213
- btn_3 = gr.Button()
1214
- gr.Markdown("## 重新生成問題")
1215
- btn_create_question = gr.Button("Create Questions")
1216
- with gr.Tab("markdown"):
1217
- gr.Markdown("## 請複製以下 markdown 並貼到你的心智圖工具中,建議使用:https://markmap.js.org/repl")
1218
- mind_map = gr.Textbox(container=True, show_copy_button=True, lines=40, elem_id="mind_map_markdown")
1219
- with gr.Tab("心智圖",elem_id="mind_map_tab"):
1220
- mind_map_html = gr.HTML()
1221
-
1222
- with gr.Row():
1223
- gr.Markdown("## 教育評量饗宴")
1224
- with gr.Row():
1225
- with gr.Column(scale=2):
1226
- with gr.Tab("認知階層評量題目"):
1227
- cognitive_level_content = gr.Textbox(label="輸入學習目標與內容")
1228
- cognitive_level_content_btn = gr.Button("生成評量題目")
1229
- with gr.Tab("素養導向閱讀題組"):
1230
- literacy_oriented_reading_content = gr.Textbox(label="輸入閱讀材料")
1231
- literacy_oriented_reading_content_btn = gr.Button("生成閱讀理解題")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1232
  with gr.Tab("學習單"):
1233
- worksheet_content = gr.Textbox(label="輸入學習單內容")
1234
- worksheet_content_btn = gr.Button("生成學習單")
1235
- with gr.Tab("自我評估"):
1236
- self_assessment_content = gr.Textbox(label="輸入自評問卷或檢查表")
1237
- self_assessment_content_btn = gr.Button("生成自評問卷")
1238
- with gr.Tab("自我反思評量"):
1239
- self_reflection_content = gr.Textbox(label="輸入自我反思活動")
1240
- self_reflection_content_btn = gr.Button("生成自我反思活動")
1241
- with gr.Tab("後設認知"):
1242
- metacognition_content = gr.Textbox(label="輸入後設認知相關問題")
1243
- metacognition_content_btn = gr.Button("生成後設認知問題")
1244
- with gr.Column(scale=3):
1245
- # 生成對應不同模式的結果
1246
- exam_result = gr.Textbox("生成結果")
1247
-
1248
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1249
 
1250
- # 傳統模式
1251
- # send_button.click(
1252
- # respond,
1253
- # inputs=[msg, df_string_output, chatbot, socratic_mode_btn],
1254
- # outputs=[msg, chatbot]
1255
- # )
1256
- # # 连接按钮点击事件
1257
- # btn_1.click(respond, inputs=[btn_1, df_string_output, chatbot, socratic_mode_btn], outputs=[msg, chatbot])
1258
- # btn_2.click(respond, inputs=[btn_2, df_string_output, chatbot, socratic_mode_btn], outputs=[msg, chatbot])
1259
- # btn_3.click(respond, inputs=[btn_3, df_string_output, chatbot, socratic_mode_btn], outputs=[msg, chatbot])
1260
-
1261
- # chat_with_youtube_transcript
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1262
  send_button.click(
1263
- chat_with_youtube_transcript,
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
- chat_with_youtube_transcript,
1270
- inputs=[video_id, thread_id, df_string_output, btn_1, chatbot, socratic_mode_btn],
1271
  outputs=[msg, chatbot, thread_id]
1272
  )
1273
  btn_2.click(
1274
- chat_with_youtube_transcript,
1275
- inputs=[video_id, thread_id, df_string_output, btn_2, chatbot, socratic_mode_btn],
1276
  outputs=[msg, chatbot, thread_id]
1277
  )
1278
  btn_3.click(
1279
- chat_with_youtube_transcript,
1280
- inputs=[video_id, thread_id, df_string_output, btn_3, chatbot, socratic_mode_btn],
1281
  outputs=[msg, chatbot, thread_id]
1282
  )
1283
 
1284
- btn_create_question.click(change_questions, inputs = [df_string_output], outputs = [btn_1, btn_2, btn_3])
 
 
 
 
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
- video_id,
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
- video_id,
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)