
    ji+>                     v   d Z ddlZddlZddlZddlZddlmZ ddlmZ ddl	Z	ddl
mZ ddlmZ ddlmZ ddlmZ dd	lmZ dd
lmZmZ ddlmZmZmZmZmZ ddlmZ  ej@                  d      Z!dZ"da#d Z$de%de&e'e(   e)f   fdZ*dZ+de%de(fdZ,dede%ddfdZ-deddfdZ.deddfdZ/deddfdZ0deddfdZ1y)u  
會議處理流水線(階段 4):音訊 → 逐字稿 → 摘要 → 待辦事項 → 推 Coba 訊息。

執行流程:
  1. POST /api/meetings 把音訊存到磁碟,建 Meeting record(status=uploaded)
  2. 觸發背景任務 process_meeting()
  3. status=transcribing → 呼叫 faster-whisper 跑本地語音辨識
  4. status=summarizing → 把逐字稿丟給 Claude(Sonnet)產出摘要 + 決策 + 待辦
  5. status=completed → Coba 在團隊頻道發一則「會議摘要」訊息

Whisper 模型:
  - 預設 small(244MB,中文準度夠,CPU 約 1x realtime)
  - 第一次跑會自動下載到 ~/.cache/huggingface/

成本:Whisper 本地跑免費,Claude 摘要 ~$0.01-0.05/場
    N)Path)UUID)select)selectinload)settings)manager)AsyncSessionLocal)ChannelMessage)MeetingMeetingActionItemMeetingDecisionMeetingSummaryMeetingTranscript)Userzcoba.meetingsmallc                      t         Dddlm}  t        j	                  dt
                | t
        dd      a t        j	                  d       t         S )u9   延遲載入 Whisper 模型(第一次會下載 ~244MB)。r   WhisperModelu.   載入 Whisper(%s)模型,第一次會下載...cpuint8)devicecompute_typeu   Whisper 模型 ready)_whisper_modelfaster_whisperr   loggerinfoWHISPER_MODEL_SIZEr   s    B/volume1/homes/robertsu/coba/app/app/services/meeting_processor.py_get_whisper_modelr    2   s@     /DFXY%&8U[\*+    
audio_pathreturnc                    t               }|j                  | ddd      \  }}g }|D ]O  }|j                  |j                  |j                  |j
                  j                         t        |dd      d       Q ||j                  fS )uL   同步在 thread 裡跑 Whisper,回傳 (segments, total_duration_seconds)。zh   T)language	beam_size
vad_filteravg_logprobN)startendtext
confidence)	r    
transcribeappendr+   r,   r-   stripgetattrduration)r"   modelsegments_iterr   segmentsss         r   _transcribe_syncr8   >   s     E**	 + M4 HWW55FFLLN!!]D9	
 	  T]]""r!   u  你是會議紀錄助手。我會給你一場會議的逐字稿,請你產出嚴格 JSON 格式的摘要。

【輸出格式】
{
  "executive_summary": "三句話以內,把整場會議的核心結論濃縮給沒參加的人看",
  "key_topics": ["主題1", "主題2", ...],
  "decisions": [
    {"decision": "決定了什麼", "context": "為什麼這樣決定(可省略)"}
  ],
  "action_items": [
    {"description": "誰要做什麼", "suggested_assignee_name": "建議負責人姓名(若逐字稿有提到)", "suggested_due_date": "YYYY-MM-DD 或 null"}
  ]
}

【規則】
- 用繁體中文
- decisions 跟 action_items 沒有就給空陣列
- 嚴格 JSON 格式,不要 markdown 程式碼框,不要解釋
rawc                     | j                         } t        j                  d|       }|si S 	 t        j                  |j                  d            S # t        j                  $ r i cY S w xY w)u%   容錯解析 Claude 回傳的 JSON。z\{[\s\S]*\}r   )r1   researchjsonloadsgroupJSONDecodeError)r9   ms     r   _parse_summary_jsonrB   g   s[    
))+C
		.#&A	zz!''!*%% 	s   #A A('A(
meeting_idstatusc                 d   K   t        j                  dt        |       |d       d{    y7 w)uD   推送會議狀態變化(讓前端可以顯示「處理中...」)。meeting_status)typerC   rD   N)
ws_manager	broadcaststr)rC   rD   s     r   _broadcast_statusrK   t   s0     


 *o    s   &0.0meetingc                 
  K   | j                  t        t              j                  t        j                  dk(  t        j
                  j                  d      t        j                  j                  d                   d{   j                         }|| j                  t        t              j                  t        j
                  j                  d      t        j                  j                  d            j                  d             d{   j                         }|t        j                  d       y| j                  t        t              j                  t        j                  |j                  k(               d{   j                         }t        | j                  t        t               j                  t         j                  |j                  k(        j#                  t         j$                               d{   j'                         j)                               }t        | j                  t        t*              j                  t*        j                  |j                  k(        j#                  t*        j$                               d{   j'                         j)                               }t-        |j                        |j.                  |r|j0                  nd|r|j2                  ng |D cg c]/  }t-        |j                        |j4                  |j6                  d1 c}|D cg c]s  }t-        |j                        |j8                  |j:                  rt-        |j:                        nd|j<                  |j>                  rt-        |j>                        nddu c}d	}tA        |j                  dtC        jD                  |d
      d      }	| jG                  |	       | jI                          d{    | jK                  |	       d{    |	j                  |_&        | jI                          d{    ddl'm(}
m)}m*}m+} | j                  t        t@              jY                  t[        t@        j\                              j                  t@        j                  |	j                  k(               d{   j_                         } ||j                  |        d{   } || |
||            }ta        jb                   |dt-        |j                        |d             d{    y7 7 77 7 G7 c c}w c c}w 7 V7 @7 7 7 s7 +w)u   Coba 在團隊頻道發一則特殊訊息:會議摘要 + action items 按鈕。

    用 message_type='meeting_summary',content 是 JSON 字串(前端特別 render)。
    u   團隊對話TN   u.   找不到任何預設頻道,跳過摘要訊息 )iddecisioncontext)rP   descriptionsuggested_assignee_idsuggested_due_dateconverted_task_id)rC   titlesummary
key_topics	decisionsaction_itemsF)ensure_asciimeeting_summary)
channel_iduser_idcontentmessage_typer   )_build_read_by_get_channel_read_states_serialize_message_ws_safemessage_new)rG   r^   message)2executer   r
   wherename
is_defaultis_
deleted_atscalar_one_or_nonelimitr   warningr   rC   rP   listr   order_byorder_indexscalarsallr   rJ   rW   executive_summaryrY   rQ   rR   rS   rT   rU   rV   r   r=   dumpsaddcommitrefreshsummary_message_idapp.api.messagesrb   rc   rd   re   optionsr   	reactions
scalar_onerH   rI   )dbrL   chrX   rZ   r[   dapayloadmsgrb   rc   rd   re   msg_fullmember_reads
serializeds                    r   _post_summary_to_chatr   }   s6     

wLLN*""4(""4(	
    
zJJ7O!!""&&t,""&&t, eAh	
 

  	 
zGH ZZ~$$^%>%>'**%LM   BJJ	))WZZ7	8	/--	.  wy	I
 rzz !	 ++wzz9	:	#//	0   wy	L '**o077,,R,3g((_hi_hZ[SYAJJ199U_hi "	
 " !$$i }}IJI`I`Q-D-D)Efj&'&:&:ABATATS)<)<%=Z^ "	
G$ 55

77&	C FF3K
))+
**S/!$G
))+  jjw	g//0	1	wzzSVV#	$  z|	 
 2"%%<<L#HnX|.TUJ


x"%%j)     _

 j	
&  
 =s   A>U+ UBU+UA5U+U	A?U+UBU+UA%U+=4U1U+7A8U/AU+UU+U (U+U"	BU+
U%(U+3U'4A	U+=U)>U+U+	U+U+U+U+U+"U+%U+'U+)U+c                   K   t               4 d{   }|j                  t        t              j	                  t        j
                  | k(               d{   j                         }||j                  s	 ddd      d{    y	 d|_        |j                          d{    t        | d       d{    t        j                  d|j                         t        j                  t        |j                         d{   \  }}||_        t#        |      D ]2  \  }}|j%                  t'        | d|d   |d   |d   |d   |	             4 |j                          d{    t        j                  d
t)        |      |       dj+                  d |D              j-                         }|sLd|_        d|_        |j                          d{    t        | d       d{    	 ddd      d{    yd|_        |j                          d{    t        | d       d{    t0        j2                  sLd|_        d|_        |j                          d{    t        | d       d{    	 ddd      d{    yt5        j6                  t0        j2                        }d| }	|j8                  j;                  t0        j<                  dt>        d|	dg       d{   }
dj+                  d |
j@                  D              }tC        |      }tE        | |jG                  dd      |jG                  d      xs g t0        j<                  |
jH                  jJ                  |
jH                  jL                        }|j%                  |       t#        |jG                  d      xs g       D ]X  \  }}tO        |tP              s|jG                  d      s)|j%                  tS        | |d   |jG                  d      |              Z tU        |j                  t        tV              j	                  tV        jX                  j[                  d      tV        j\                  j[                  d!                   d{   j_                         ja                               }|D ci c]'  }|jb                  je                         |j
                  ) }}t#        |jG                  d"      xs g       D ]  \  }}tO        |tP              r|jG                  d#      s(d}|jG                  d$      xs dj-                         je                         }|r	||v r||   }|jG                  d%      xs d}|r!tg        jh                  d&tk        |            sd}|j%                  tm        | |d#   |||'              d(|_        |j                          d{    t        | d(       d{    to        | |       d{    |
jH                  jJ                  d)z  d*z  |
jH                  jL                  d)z  d+z  z   }t        j                  d,tk        |       dd- |
jH                  jJ                  |
jH                  jL                  |       tq        ||       d{    ddd      d{    y7 7 [7 17 7 7 7 Z7 7 7 7 7 7 o7 ^7 P7 7 Yc c}w 7 7 7 7 T# tr        $ rs}t        ju                  d.       d|_        tw        |      jx                   d/| |_        |j                          d{  7   t        | d       d{  7   Y d}~d}~ww xY w7 # 1 d{  7  sw Y   yxY ww)0u4   背景任務:把上傳的會議從頭處理到尾。Ntranscribingu   開始轉逐字稿:%s	speaker_0r+   r,   r-   r.   )rC   speaker_label
start_timeend_timer`   r.   rs   u!   逐字稿完成:%d 段 / %.1f 秒
c              3   &   K   | ]	  }|d      ywr-   N ).0r7   s     r   	<genexpr>z"process_meeting.<locals>.<genexpr>   s     !>X!F)Xs   failedu2   音訊辨識不到任何內容(可能是靜音檔)summarizingu   ANTHROPIC_API_KEY 未設定)api_keyu1   以下是會議逐字稿,請產出 JSON 摘要:

i   user)roler`   )r4   
max_tokenssystemmessagesrO   c              3   T   K   | ]   }|j                   d k(  s|j                   " ywr   )rG   r-   )r   bs     r   r   z"process_meeting.<locals>.<genexpr>  s"     O*:Qaff>N!&&*:s   ((rv   u   (無法產生摘要)rY   )rC   rv   rY   generated_byinput_tokensoutput_tokensrZ   rQ   rR   )rC   rQ   rR   rs   Tr[   rS   suggested_assignee_namerU   z^\d{4}-\d{2}-\d{2}$)rC   rS   rT   rU   rs   	completedi@B g      @g      .@u2   會議處理完成 id=%s tokens=%d/%d cost=US$%.4f   u   會議處理失敗z: )=r	   rh   r   r   ri   rP   rn   r"   rD   ry   rK   r   r   asyncio	to_threadr8   audio_duration_seconds	enumeraterx   r   lenjoinr1   error_messager   ANTHROPIC_API_KEY	anthropicAsyncAnthropicr   createCLAUDE_MODEL_SONNETSUMMARY_SYSTEM_PROMPTr`   rB   r   getusager   r   
isinstancedictr   rq   r   rm   rl   	is_activert   ru   display_namelowerr;   matchrJ   r   _convert_action_items_to_tasksr   	Exception	exceptionrG   __name__)rC   r   rL   r6   r3   iseg	full_textclientuser_promptresponser9   datarX   r   usersuuser_by_namer   assignee_id	name_hintduecostes                           r   process_meetingr      sf     ""b7O!!'**
":;
 
 	 ?'"4"4 #""{	:+GN))+#J???KK/1C1CD'.'8'89I7K]K]'^!^Hh-5G*#H-3()"-"7| ZK"<0 !  . ))+KK;S]HU 		!>X!>>DDFI!)(\%iik!!'
H===I #""N +GN))+#J>>>--!)(E%iik!!'
H===a #""d --h6P6PQFOPY{[K#__3322,#)kBC	 4  H ''O(*:*:OOC&s+D %%"&((+>@V"W88L17R%99%^^88&nn::G FF7O "$((;"7"=2>1a&155+<FF?#-!": !i 0$%	  ? 

t""4??#6#6t#<dnn>P>PQU>VW! wy E CHH%QANN002ADD8%LH!$((>":"@bA1!!T*!%%2F"UU#<=CJJLRRT	l!:".y"9Kee019Trxx(>CIC() !- 0*5'* !  B$ )GN))+#J<<< 1R@@@NN//);sBhnnFbFbenFnrvEvvDKKDJ#++,, (G444w #""
 # ? "_  "=G #P >
 "=_ #h> I* < A 5 	:12%GN'+Aw'7'7&81#$>G!))+#J999	:{ #"""s(  ^Z/^A]1Z2!]19^Z5^[0%Z8&[09Z;:A[0Z>A![0'[(A-[0[[0)[*[0/^:[
;^ [0[[0.[/5[0$[%[08[9[0>^	[
^A![00[1C [0[0$B"[0[
([0/,["C<[0['[0+[*,[0?[, B[0[.[0^)]/*^2]15^8[0;[0>[0[0[0[0
^[0[0[0[0^[0[0"[0*[0,[0.[00	],9A]']
]']]'"]1'],,]1/^1^7]:8^?^c                 @    t        j                  t        |              y)u:   同步入口,啟背景任務不 block HTTP 上傳回應。N)r   create_taskr   )rC   s    r   trigger_meeting_processingr   `  s    
34r!   c                   K   ddl m} ddlm} ddlm} t        |j                  t        |      j                  |j                  | k(  |j                  j                  d                   d{   j                         j                               }|sy|j                  t        t              j                  t        j                   | k(               d{   j#                         }|r|j$                  nd}|t&        j)                  d|        yg }|D ]  }		 |	j*                  }
t-        |
t.              r'ddlm} 	 |j3                  |
d      j5                         }
 ||	j8                  dd	 d
|j:                   dd|
|	j<                  |d      }|j?                  |       |jA                          d{    |j                   |	_        |jC                  |j:                         |j?                   |d|d|j:                   t/        |j                         t/        |       dd              |jI                          d{    |r1t&        jK                  dt/        |       dd tM        |      |dd        yy7 7 # t6        $ r d}
Y 4w xY w7 # tD        $ r t&        jG                  d       Y w xY w7 ww)uE   讀 meeting 的 action_items,每筆建一個 Task(若還沒對應)。r   )Task)r   )AgentActivityNu5   meeting %s 沒 uploaded_by,action_item 無法建 task)datetimez%Y-%m-%d   u   從會議「u   」自動產出 (action_item)mediumtodo)rW   rS   prioritydue_dater   
created_byrD   task_create_autou   會議自動轉任務:meeting_action_item)task_idrC   via)activity_typer_   rX   extrau   action_item 轉 task 失敗u$   meeting %s 自動建 %d 個任務:%sr      )'app.models.taskr   app.models.meetingr   app.models.agent_activityr   rq   rh   r   ri   rC   rV   rl   rt   ru   r   rP   rn   uploaded_byr   rp   rU   r   rJ   r   strptimedate
ValueErrorrS   rW   rT   rx   flushr0   r   r   ry   r   r   )rC   r   r   r   r   itemsrL   
creator_idcreated_titlesair   r   ts                r   r   r   j  sr    $47

 !''((J6//33D9	
 
 wyE ZZwgjjJ67   )0$$TJNPZ[N 	<''C#s#-"++C<AACC nnTc**7==/9VW!44%A FF1I((*#$44B !!!''*FF=0"0	:"144y"%j/0		 	/ F ))+2
OBQ^!4nRa6H	
 q$ " C   	<:;	<s   A+J?-I>.A(J?JAJ?"J> JAJ6J7A8J/J?J=9J?J?JJJJJ:6J?9J::J?)2__doc__r   r=   loggingr;   pathlibr   uuidr   r   
sqlalchemyr   sqlalchemy.ormr   
app.configr   app.core.ws_managerr   rH   app.databaser	   app.models.channelr
   r   r   r   r   r   r   r   app.models.userr   	getLoggerr   r   r   r    rJ   tuplerq   r   floatr8   r   rB   rK   r   r   r   r   r   r!   r   <module>r      s  "    	     '  5 * /  ! 
		>	*  	# #tDz5/@)A #( *
S 
T 
 c d YW Y YxD:d D:t D:N54 5D 5B
T B
$ B
r!   