# ============================ # utils.py — Utility Functions # ============================ import os import shutil from fastapi import UploadFile from moviepy.editor import VideoFileClip from pydub import AudioSegment, effects import pandas as pd from collections import Counter import time from config import UPLOAD_FOLDER from models import pipelines, models, together def save_uploaded_file(file: UploadFile) -> str: os.makedirs(UPLOAD_FOLDER, exist_ok=True) filepath = os.path.join(UPLOAD_FOLDER, file.filename) with open(filepath, "wb") as f: shutil.copyfileobj(file.file, f) return filepath def extract_and_normalize_audio(file_path: str) -> str: ext = os.path.splitext(file_path)[1].lower() audio_path = os.path.join(UPLOAD_FOLDER, "extracted_audio.wav") if ext == ".mp4": clip = VideoFileClip(file_path) clip.audio.write_audiofile(audio_path) elif ext in [".mp3", ".wav"]: audio_path = file_path else: raise ValueError("รองรับเฉพาะไฟล์ mp4, mp3, wav เท่านั้น") audio = AudioSegment.from_file(audio_path) normalized_audio = effects.normalize(audio) cleaned_path = os.path.join(UPLOAD_FOLDER, "cleaned.wav") normalized_audio.export(cleaned_path, format="wav") return cleaned_path def diarize_audio(audio_path: str) -> pd.DataFrame: pipeline = pipelines[0] diarization = pipeline(audio_path) return pd.DataFrame([ {"start": round(turn.start, 3), "end": round(turn.end, 3), "speaker": speaker} for turn, _, speaker in diarization.itertracks(yield_label=True) ]) def split_segments(audio_path: str, df: pd.DataFrame) -> str: segment_folder = os.path.join(UPLOAD_FOLDER, "segments") if os.path.exists(segment_folder): shutil.rmtree(segment_folder) os.makedirs(segment_folder, exist_ok=True) audio = AudioSegment.from_file(audio_path) for i, row in df.iterrows(): start_ms = int(row['start'] * 1000) end_ms = int(row['end'] * 1000) segment = audio[start_ms:end_ms] filename = f"segment_{i:03d}_{row['speaker']}.wav" segment.export(os.path.join(segment_folder, filename), format="wav") return segment_folder def transcribe_segments(segment_folder: str) -> pd.DataFrame: files = sorted(os.listdir(segment_folder)) model = models[0] # สมมติว่ามี faster-whisper model อยู่แล้ว all_word_probs = [] for filename in files: segment_path = os.path.join(segment_folder, filename) segments, _ = model.transcribe( segment_path, language="th", beam_size=5, vad_filter=True, word_timestamps=True ) for seg in segments: if hasattr(seg, "words"): for word in seg.words: if word.probability is not None: all_word_probs.append({ "filename": filename, "word": word.word, "start": word.start, "end": word.end, "probability": round(word.probability, 4) }) return pd.DataFrame(all_word_probs) def clean_summary(text): import re if not text or len(str(text).strip()) == 0: return "ไม่มีข้อมูลสำคัญที่จะสรุป" text = str(text) patterns_to_remove = [ r'สรุป:\s*', r'สรุปการประชุม:\s*', r'บทสรุป:\s*', r'ข้อสรุป:\s*', r'\*\*Key Messages:\*\*|\*\*หัวข้อหลัก:\*\*', r'\*\*Action Items:\*\*|\*\*ประเด็นสำคัญ:\*\*', r'\*\*Summary:\*\*|\*\*สรุป:\*\*', r'^[-•]\s*Key Messages?:?\s*', r'^[-•]\s*Action Items?:?\s*', r'^[-•]\s*หัวข้อหลัก:?', r'^[-•]\s*ประเด็นสำคัญ:?', r'^[-•]\s*ข้อมูลน่าสนใจ:?', r'^[-•]\s*บทสรุป:?', r'\r\n|\r|\n', r'\t+', r'หมายเหตุ:.*?(?=\n|\r|$)', r'เนื่องจาก.*?(?=\n|\r|$)', r'ไม่มีข้อความ.*?(?=\n|\r|$)', r'ไม่มีประเด็น.*?(?=\n|\r|$)', r'ไม่มี Action Items.*?(?=\n|\r|$)', r'ไม่มีรายการ.*?(?=\n|\r|$)', r'ต้องการข้อมูลเพิ่มเติม.*?(?=\n|\r|$)', r'ต้องขอความชัดเจนเพิ่มเติม.*?(?=\n|\r|$)', r'\(ตัดประโยคที่ไม่เกี่ยวข้องหรือซ้ำซ้อนออก.*?\)', r'\(.*?เพื่อเน้นความชัดเจน.*?\)', r'ตามที่ได้กล่าวไว้.*?(?=\n|\r|$)', r'จากข้อความที่ให้มา.*?(?=\n|\r|$)', r'Based on the provided text.*?(?=\n|\r|$)', r'According to the text.*?(?=\n|\r|$)', r'\s+' ] cleaned_text = text for pattern in patterns_to_remove: if pattern == r'\s+': cleaned_text = re.sub(pattern, ' ', cleaned_text) else: cleaned_text = re.sub(pattern, '', cleaned_text, flags=re.IGNORECASE | re.MULTILINE | re.DOTALL) cleaned_text = re.sub(r'\*\*(.*?)\*\*', r'\1', cleaned_text) cleaned_text = re.sub(r'\*(.*?)\*', r'\1', cleaned_text) cleaned_text = re.sub(r'_{2,}(.*?)_{2,}', r'\1', cleaned_text) cleaned_text = re.sub(r'[.]{3,}', '...', cleaned_text) cleaned_text = re.sub(r'[!]{2,}', '!', cleaned_text) cleaned_text = re.sub(r'[?]{2,}', '?', cleaned_text) cleaned_text = re.sub(r'^[-•*]\s*', '', cleaned_text, flags=re.MULTILINE) cleaned_text = re.sub(r'^\d+\.\s*', '', cleaned_text, flags=re.MULTILINE) useless_phrases = [ 'ไม่มี', 'ไม่สามารถสรุปได้', 'ข้อความต้นฉบับไม่มีความหมาย', 'ไม่มีข้อมูลเพียงพอ', 'ไม่มีประเด็นสำคัญ', 'ไม่มี Action Items', 'ต้องขอความชัดเจนเพิ่มเติม', 'ไม่มีข้อมูลที่สำคัญ', 'ไม่สามารถระบุได้', 'ข้อมูลไม่ชัดเจน', 'ไม่มีเนื้อหาที่เกี่ยวข้อง', 'N/A', 'n/a', 'Not applicable', 'No content', 'No summary available' ] cleaned_text = cleaned_text.strip() if (len(cleaned_text) < 15 or any(phrase.lower() in cleaned_text.lower() for phrase in useless_phrases) or cleaned_text.lower() in [phrase.lower() for phrase in useless_phrases]): return "ไม่มีข้อมูลสำคัญที่จะสรุปมากพอ" cleaned_text = re.sub(r'\s+([.!?])', r'\1', cleaned_text) cleaned_text = re.sub(r'([.!?])\s*([A-Za-zก-๙])', r'\1 \2', cleaned_text) return cleaned_text def summarize_texts(texts, api_key, model="deepseek-ai/DeepSeek-V3", delay=1): summaries = [] for idx, text in enumerate(texts): prompt = f""" สรุปข้อความประชุมนี้เป็นภาษาไทยสั้น ๆ เน้นประเด็นสำคัญ (key messages) และ Action Items โดยตัดรายละเอียดที่ไม่สำคัญออก:\nข้อความ:\n{text}\nสรุป:\n- Key Messages:\n- Action Items:\n""" try: response = together.chat.completions.create( model=model, messages=[ {"role": "system", "content": "คุณเป็นผู้เชี่ยวชาญในการสรุปเนื้อหา ตอบเป็นภาษาไทยเสมอ เน้นหัวข้อหลักและข้อมูลสำคัญ"}, {"role": "user", "content": prompt} ], max_tokens=1024, temperature=0.7, ) summary = response.choices[0].message.content.strip() summary = clean_summary(summary) summaries.append(summary) except Exception as e: print(f"Error at index {idx}: {e}") summaries.append("ไม่สามารถสรุปได้") if idx < len(texts) - 1: time.sleep(delay) return summaries