From f5a22c0a7f286f075add2cbcc2eb380107dcb7e8 Mon Sep 17 00:00:00 2001
From: Bifang <915779419@qq.com>
Date: Sat, 9 May 2026 16:52:09 +0800
Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=A7=A3=E6=9E=90=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/assets/app.js | 111 ++++++++++++++++++++++-
frontend/index.html | 1 +
meeting_summary.py | 11 +++
template/template2.md | 12 ---
template_guides/template1.md | 10 +++
template_guides/template2.md | 7 ++
web/__pycache__/server.cpython-314.pyc | Bin 27405 -> 38260 bytes
web/server.py | 118 ++++++++++++++++++++++---
8 files changed, 245 insertions(+), 25 deletions(-)
create mode 100644 template_guides/template1.md
create mode 100644 template_guides/template2.md
diff --git a/frontend/assets/app.js b/frontend/assets/app.js
index be5fb07..f2c8534 100644
--- a/frontend/assets/app.js
+++ b/frontend/assets/app.js
@@ -91,10 +91,18 @@ function meetingById(meetingId) {
return state.meetings.find((item) => item.id === meetingId) || null;
}
+function templateMetaByName(name) {
+ return state.templates.find((item) => item.name === name) || null;
+}
+
function isMarkdownFile(name = "") {
return name.toLowerCase().endsWith(".md");
}
+function templateNameFromGuideName(name) {
+ return name;
+}
+
function renderMeetingStatus(meeting) {
const name = $("#sidebar-meeting-name");
const meta = $("#sidebar-meeting-meta");
@@ -273,6 +281,17 @@ function renderNode(node, parent, depth) {
label.textContent = node.name;
row.appendChild(label);
+ if (node.delete_mode === "template") {
+ const del = document.createElement("span");
+ del.className = "del-btn";
+ del.textContent = "删除";
+ del.addEventListener("click", async (event) => {
+ event.stopPropagation();
+ await deleteTemplateNode(node.name);
+ });
+ row.appendChild(del);
+ }
+
row.addEventListener("click", async () => {
await openTreeResource(node.path);
});
@@ -351,17 +370,31 @@ function syncTemplateSelection(name) {
$("#tpl-select").value = name;
}
+function updateGuideButton(resource) {
+ const button = $("#btn-reparse-guide");
+ if (!resource || (resource.type !== "template" && resource.type !== "template-guide")) {
+ button.disabled = true;
+ button.textContent = "生成解析说明";
+ return;
+ }
+
+ const hasGuide = resource.type === "template-guide" || Boolean(resource.hasGuide);
+ button.disabled = false;
+ button.textContent = hasGuide ? "重新生成说明" : "生成解析说明";
+}
+
function applyRightResource(resource) {
state.rightResource = resource;
$("#editor-resource-label").textContent = `当前资源:${resource.label}`;
$("#side-editor").value = resource.content || "";
$("#btn-toggle-side-edit").disabled = !resource.editable;
$("#btn-toggle-side-edit").textContent = resource.editable ? "编辑资源" : "只读资源";
+ updateGuideButton(resource);
state.rightEditMode = false;
renderSidePreview(resource);
- if (resource.type === "template") {
- syncTemplateSelection(resource.name);
+ if (resource.type === "template" || resource.type === "template-guide") {
+ syncTemplateSelection(resource.templateName || resource.name);
}
}
@@ -396,8 +429,25 @@ async function openTemplate(name, treeKey = `file:templates/${name}`) {
await openRightResource({
type: "template",
name,
+ templateName: name,
label: `模板 / ${name}`,
content: data.content,
+ hasGuide: Boolean(data.has_guide),
+ editable: true,
+ treeKey,
+ });
+}
+
+async function openTemplateGuide(name, treeKey = `file:template_guides/${name}`) {
+ const templateName = templateNameFromGuideName(name);
+ const data = await api(`/api/templates/${encodeURIComponent(templateName)}/guide`);
+ await openRightResource({
+ type: "template-guide",
+ name,
+ templateName,
+ label: `模板说明 / ${templateName}`,
+ content: data.content,
+ hasGuide: true,
editable: true,
treeKey,
});
@@ -458,6 +508,10 @@ async function openTreeResource(path) {
await openPrompt(parts.slice(1).join("/"), treeKey);
return;
}
+ if (group === "template_guides") {
+ await openTemplateGuide(parts.slice(1).join("/"), treeKey);
+ return;
+ }
const meetingId = parts[1];
const filename = parts.slice(2).join("/");
@@ -509,6 +563,29 @@ async function deleteMeetingNode(meetingId, deleteMode) {
}
}
+async function deleteTemplateNode(name) {
+ if (!window.confirm(`确定删除模板 ${name} 及其对应使用说明吗?`)) {
+ return;
+ }
+
+ await api(`/api/templates/${encodeURIComponent(name)}`, { method: "DELETE" });
+ toast(`模板已删除:${name}`);
+
+ if (
+ state.rightResource &&
+ (state.rightResource.templateName === name || state.rightResource.name === name)
+ ) {
+ state.rightResource = null;
+ }
+
+ if (state.templateName === name) {
+ state.templateName = "";
+ savePreferences({ templateName: "" });
+ }
+
+ await refresh();
+}
+
function initResize(gutterId, leftId, rightId) {
const gutter = document.getElementById(gutterId);
const left = document.getElementById(leftId);
@@ -619,6 +696,15 @@ async function refresh() {
return;
}
+ if (
+ state.rightResource &&
+ state.rightResource.type === "template-guide" &&
+ templateData.some((item) => item.name === state.rightResource.templateName)
+ ) {
+ await openTemplateGuide(state.rightResource.name, state.rightResource.treeKey);
+ return;
+ }
+
if (!state.rightResource && state.templateName) {
await openTemplate(state.templateName);
}
@@ -743,6 +829,12 @@ $("#btn-toggle-side-edit").addEventListener("click", async () => {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
});
+ } else if (resource.type === "template-guide") {
+ await api(`/api/templates/${encodeURIComponent(resource.templateName)}/guide`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ content }),
+ });
} else if (resource.type === "prompt") {
await api(`/api/prompts/${encodeURIComponent(resource.name)}`, {
method: "PUT",
@@ -768,6 +860,21 @@ $("#btn-open-template").addEventListener("click", async () => {
await openTemplate(state.templateName);
});
+$("#btn-reparse-guide").addEventListener("click", async () => {
+ const resource = state.rightResource;
+ if (!resource || (resource.type !== "template" && resource.type !== "template-guide")) {
+ return;
+ }
+
+ const templateName = resource.templateName || resource.name;
+ const result = await api(`/api/templates/${encodeURIComponent(templateName)}/guide/reparse`, {
+ method: "POST",
+ });
+ toast(`模板说明已重新解析:${templateName}`);
+ await refresh();
+ await openTemplateGuide(result.name);
+});
+
$("#btn-import").addEventListener("click", () => {
$("#import-name").value = "";
$("#import-file").value = "";
diff --git a/frontend/index.html b/frontend/index.html
index 940c1e2..a710ac7 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -79,6 +79,7 @@
+
diff --git a/meeting_summary.py b/meeting_summary.py
index fe1026e..4344ce0 100644
--- a/meeting_summary.py
+++ b/meeting_summary.py
@@ -11,6 +11,7 @@ DATA_DIR = PROJECT_ROOT / "data" / "meetings"
RESULTS_MD_DIR = PROJECT_ROOT / "data" / "results" / "md"
RESULTS_JSON_DIR = PROJECT_ROOT / "data" / "results" / "json"
TEMPLATE_DIR = PROJECT_ROOT / "template"
+TEMPLATE_GUIDE_DIR = PROJECT_ROOT / "template_guides"
EXAMPLES_DIR = PROJECT_ROOT / "examples"
@@ -52,6 +53,13 @@ def read_template(template_name: str) -> str:
return template_path.read_text(encoding="utf-8")
+def read_template_guide(template_name: str) -> str:
+ guide_path = TEMPLATE_GUIDE_DIR / template_name
+ if not guide_path.exists():
+ return ""
+ return guide_path.read_text(encoding="utf-8")
+
+
def collect_stream(response) -> str:
content = []
current_part = None
@@ -110,6 +118,7 @@ def main():
target_name, transcript, transcript_path = load_transcript(args)
template = read_template(args.template)
+ template_guide = read_template_guide(args.template)
prompt = load_prompt("meeting_summary", "zh")
print(f"Processing transcript: {transcript_path}")
@@ -121,6 +130,8 @@ def main():
sub_topics = collect_stream(get_qwen_response(args.model, system_prompt, user_prompt))
system_prompt = prompt["system"]["role"] + prompt["mode_contracts"]["data_summary"].format(template=template)
+ if template_guide:
+ system_prompt += f"\n\n模板使用说明:\n{template_guide}"
user_prompt = prompt["user_template"]["article_summary"].format(article=transcript, sub_topices=sub_topics)
summary_text = collect_stream(get_qwen_response(args.model, system_prompt, user_prompt))
diff --git a/template/template2.md b/template/template2.md
index f1fa5d4..36eaba3 100644
--- a/template/template2.md
+++ b/template/template2.md
@@ -80,15 +80,3 @@
- XXX
- XXX
- XXX
-
----
-
-📝 **使用说明(输出时请删除):**
-- 会议内容第一部分以一段文字根据实际汇报人员和回报内容总结关键汇报内容
-- 保留一级结构,二级和三级内容根据实际会议内容动态生成
-- 请将 `X`、`XX` 等占位符替换为实际内容
-- 会议中没被提到的占位符保留,不做替换
-- 领导强调方面总结为精简的词组,不宜过长
-- 无事实依据支撑的小节、问答、决策或行动项直接省略
-- 不要为了凑模板强行写“无相关内容”
-- 议题数量可增减,不强制必须正好 3 个
diff --git a/template_guides/template1.md b/template_guides/template1.md
new file mode 100644
index 0000000..13a70d7
--- /dev/null
+++ b/template_guides/template1.md
@@ -0,0 +1,10 @@
+- 严格保留模板的Markdown标题层级(## 主模块、### 子模块),主模块标题可保留原模板的Emoji标识。
+- 所有方括号 `[]` 内的文本、示例标题(如“议题1/2/3”“关于产品定位”)及括号提示语均为结构占位符,生成时必须替换为真实会议内容,严禁原样输出。
+- 若某模块或子项在会议实际内容中无对应信息,应直接省略该部分,不得留空或强行补全占位符。
+- 各模块下的子条目数量(如议题数量、观点子项、行动项等)允许根据实际讨论内容动态增减,不强制匹配模板示例数量。
+- 保留问答对的固定排版:使用 `Q: ` 与 `A: ` 标识问答内容,不同问答对之间以 `---` 分隔。
+- 保留观点陈述的引用块格式:统一使用 `> ` 开头包裹具体观点或共识内容。
+- 保留行动项的复选框格式:统一使用 `- [ ] ` 开头列出后续待办任务。
+- 保留分析类与决策类内容的键值对列表格式:使用 `- 标签: 内容` 的结构呈现各项分析或决议。
+- 子模块标题可根据实际议题语义进行重命名,但需维持原模板的模块分类逻辑与层级归属。
+- 生成内容需严格遵循“有则保留对应格式,无则直接省略该条目”的原则,禁止输出任何未经验证的结构化空壳或示例文字。
\ No newline at end of file
diff --git a/template_guides/template2.md b/template_guides/template2.md
new file mode 100644
index 0000000..38c16b0
--- /dev/null
+++ b/template_guides/template2.md
@@ -0,0 +1,7 @@
+- 保持顶层框架(主标题、核心内容模块)固定,议题数量及中层及以下标题、子项、列表按实际内容动态增删,不强制匹配示例条目数。
+- 模板中的 `X`、`XX`、`XXX` 等占位符需优先替换为真实信息;若会议材料未提供对应内容,则原样保留占位符,严禁编造或强行替换。
+- 无事实依据支撑的空白小节、示例问答、决策或行动项必须直接省略,严禁输出“无相关内容”等凑数文字。
+- 领导强调/部署类内容需统一采用“发言人/负责人 -> 分类加粗项 -> 要点列表”的层级格式,且要点必须提炼为精简词组,避免长句。
+- 部门汇报类内容需整合为一段连贯的总结性文字,按实际汇报顺序提取关键信息与数据,不展开为多级列表。
+- 严格保留 Markdown 标题层级(`#`、`##` 等)与段落分隔符(`---`)的结构意义,确保输出排版层级清晰。
+- 输出时自动剔除模板自带的“使用说明”“提示语”等元数据,仅生成最终会议纪要正文。
\ No newline at end of file
diff --git a/web/__pycache__/server.cpython-314.pyc b/web/__pycache__/server.cpython-314.pyc
index b0df7c0e8dfe0aa74fe5d027516489f905725971..9dbcbb4248aca7c7126c54843f8974380358ddf9 100644
GIT binary patch
literal 38260
zcmd753tU^*ohN>;boD?ILgFa|7%&(cW570k8#@LAeq$^M&chCsLAHr8kSmecu$)7XF_H(ExVJ?r|wR7Yxl9ub~^LFvcOUm=2OpXCY|nocL#s$G)-pq
z@%^3ql4OCMw$t5XbMDbSk9+PpzxVl_b8DQLHV)zSJO8WyU^mD87y6+FtCG0=l?;v>
z;~wLD{2neOU-=Nvej7ptb~T2K>?(u=b~S}e>}n2~aW(9*?6!uirQA_#Id?Q;9WS@s
zk*7XNzphh~9r^k_HI?pO$Z=HY@Tw*BYe$KmR#T60N3-yJhx+_=205pc-pRPfwc8zX
z@6HZo@6HM3?9L74?#>J4?amM7vwUGs!ER5;v%4@<$bOslcy||tidfpbr+9Zss08ol
z@L5*b)#q_sXvxtfK5NpQQk2O^x>I(v%x6owQ=VGJXHU%cITC+`mfoXuW>V=2ly)ZF
zsl=VEq&v%S$CY@;=T7_;szQEt(w*hFlaq931>Pz*=}z_0@{R}fu~AdLJp9YwsLjRE
zrT0~;;=W3i-dCw*_f^VW&iM*_o{b9hhH8%1bUduSg?{xF9xVct5u}x>kmmEMwR~1z
z(MF?Ox|H)37jV9k0&c(+s`V{Fx|F5sd}TFpS+
z=k+mCQ@&ZF4^y-omi
zjwjVQggWU)S+8>fbxtPL@h#A|eywkhqt2oY9_h7Ij9G>O8!_
zbDq}fJcByVCe?Xlf#*D@)p^I!K2qa*(f5+?iwSuYfdG!BwC8ITdTK=Ho(#KXkI%9JjRjgQ2F5J#nF}e;|N(
zNIV_S+}+jH(R!vgFdXV18jM?ffhR@+!BE`x;PAkZzmG~*8{!VS`U1h>p}}Av?(PhU
z0sra#!4v8&JjLD_@`w6+sbG+PQth5$ap?4LC~iG24h@C^gMD$cKNt*zf^nhGAM(eo
zrvrfynhC}&Vjws&fDFUwzPRvs5M8l`0;h)u{GmWROHK8h80qf|1mk9FT;
zG{$vvj~O<75tE2}#oQtOB=6&oAV&`Mn4WhGKc_lzvls}Ch=cE-(Z2rP5E?fh$F%rE
zL27zwJoBl*u_M8N_+&t=8$KI%^z`@#2Z!jjdV0haD6$fN!5ly7TT)E#@LvM3LP>Xd?Ybl{Tu}vrkslf-}R0SRt
z6#%dXpV24yOg^*EQf~zCT0b+5gpSv4!njxigS|t20Ft=FKiuDQ%pVN&jEDoH#V13d
z;Vt#`YgX5-S-rY$eM8-v&6~GuUcYu@{gZ3r7Akuxa5iok8SD?PS=$h|ogV574D`^{
zjoJ@A6&P%&+fduEzPZ{Ix9>dI-nM5~PurgTt#LC0j-Z%}uE$LT#ld(6z`d_06gU%l
zhZC!jA<~#&HW-l0U?*@Vc1d_wG>^q6wPJjoF9F4^ioaKTOan;
zUn!8h^$~AVB&%7nHs7@7OP&oet9yKN%$hrXdfp;9%=4UJGP9@sx}YX5rQRBQkDm}L
zk*em!DqLg;V{gJBiA6N$K~k^?oOu2Se~ZNgQ1nah4vMfqsvv`?V@TW!qV5Gd-JB1Y
z0|-%|k>ElX|9t}R#?%H(b_524BVwS(AMEY#kDL1k`+(OXz>^bKA!@G{#I?A_ZBL2)
zz!aJ|Wj^~xP7i}TiCd7(2B1ov%Ao(rK#ww&&B$dVQGo=eQpk!5-mu`kwC>FU(Xa@j@h1PW+D!}j09
zt$WSQZx`E%24a@9DT}-Py`g^0fWi@a`uo~}na-w;j-Jl0rmj|ooMHoBgw3Q#-4)kU
zw1FZWDYImx)X2ZM3;Ar^HX}iJnHRMbhb_fX%aZGsB`RLlMZIgo-nCbDN#3;)Z%ZVr
zRkF6m9Ho=FlD$e2s(u4gTy2mM(Ztx&J?DBmx|@KCeuj#E{?a-^MgRYOR1_Q0LCj$r
zF5(V~aw%#;1O(fIOCo}0^)Lj}r)3`suvb)ZEITTcgoTnzeQ!Sg#^Y0YR~~$S&CHSm
z(nCFxP;x`~!rc=G}+-EGVV~27X3)I&29b2YzD=hQ!4vB+-Uo0V1N1
zM!tw}ujum5smedv%aVFO)rV>QBq#(Lsx(5=YKQs5ypJQAe1yuf(9Nst5w-wA7K7M|
zjCNcJphX%!qz_PPf~Z@em5I&KDs8Ja80693^9|vGN+phJjx)
zC*+K`-VnU{3#c8XrrAJOvYPjnZbU-0djdG|Ne#e_{B8g^#WFeDod&>7YCi~WRI}r5
zewZ)iy7_WQU6|wqgCBky6My>zm6u>RO(MM&$y*191A|R_#CrS`>*$_=rQ=pr)PVeJ
z9zr31zt|<`6Zc^0VB)A}^_;+{4-A~{=^f}NhIAOk*h|VI{?|gu-`L#~6%+kmtDA0^
zer~p*ZN|P!5_ZWWPH(3M;;I$+yS)^1u#_vs+jesuYHq@{gF28BIuct_^11Tf)gl;BxZB*(Eo4QRdt__bGHg}uTNqCQOiDTM9JxvM2mTpT2Gae^r
z90%hn=6qJkLg>`%J?`ggImJ+EEU{(V?SS=5f{WE=rrcw
z*&vjKxCM+H`bH=&j6irkah!#Z9_Ru~F)=xiRF7cLe*(&cowSR9=!YC0fRyMz(-Rsx
z1pxz%F*T>1iGR>3AcIz0;zI99e<%)JgOm=^nNYA7@*(sTOYg~{{$4a`f}|Ppi;v>9
z#AAr6ttcbI#8Lc=XUg4F)gwD~QvH#k^&oDAQY33!rrwhygQvt3s2rpbucA;^rl7l>
z0iE1iaS++<$b8cqlj6XDScB)o#Y>FY>
zJ@N3AU2?3OHyN{S^Hwg;6U!-%W#!DfIal3nF2iLW+kHEi%l4i##aww8GcROL_-9>Z
zQCC&iRfT(Y*ZFO)Y`bnRi)EKgjJ)~$SD%-%>t-BvH?xZ-9(?oYSC2~BD`y-lZ`;wt
zr$LfTUTn^6&f=3~TLHHjp;?W@y(#(;4fyK~5DjH|E|Ww;o_7r5L0rUTh!&&he&h#<
zZV*Km__envy4(Qyn_v4bxGgR^O++ykG}-Fw_?akMb#sZPZC}!_VZNKcFHLLt{b*Wf
zD?{l`!iBIrjfQ0eH3X*9KvkPR%Qzhf`I%y@i(q0YT8Z0aSnlfqkb_Fbtu0MmO+77p
zeCcE_g#pD`b3MZ_Gb9nj>>j2!sr;3mDW1VY*nsax0$j-DnbrbG)#o*{9@`Fp*YmsE|DC1|3@|SncvtRFqh`&9m@HGu|QJyfG
zb(Cy8_YF&+<@Zbd0TToj$}_<&4IK%>5FX0H5sj9oqu$`Jyw3>jOwhD5=<9l$M2oAf
zVh~MriG)pU;x@!Oe6?4b#V2tG)*EJr0VO^Kt4gZqXp|9nlPgw9=%6-%OhIg`MdgSV%nZjL?
z?_o*s+z=kQ8>BR>C)xM&5GbEfx3w?}had}|vXDW;K-eb|*#c70D>K@)7`PFk50)d6
z$yPQIs_aW$M7FZu&0&}Kpc&$q{2qxAC!iXGxO1%jVLp*&b*Ko0JT<7=;qU@b2kB&y
zTAMJDeVL4=X|f5)BkD5iNxqxtlHup$Xk;{R&)}2(f&RWSu)m=Zu{Tggl%?9pjD$l2
zFoivXZixiJz|t{FGty-Sd4U9$3@}d6pdeN1={?y$&?kNg#o5S_3SLCXl7M7
zvnrCgVyrD@$$a_Q7oMH4Mm;OSo)r<#${EYbIcL$tPN{rT#JO3rZjRaAV+ZaA4Q)t&
z72cn|P*X=_ET90|%9h(R3{a4kO6dxERlGBQ0^;rQkeWE`#*oqIVi)SkobLuRC#qy+o&=AJ6yIK!)>?g&1*MoanT8TrTcP}F=QR}nI7F)&|#4jV0_0q*2
zHfJxK@vfRW9kDk`LZjX{6^=BjxSJJUTIYX|V1S^!xR&A3^S?R~7pydYqzD
z6e)N|;zHcmKL|VQ!X~n_C=?_^AK{&eue?Rkdl+b3ehj?BW#-`{^0j|}<;pLofj;VL
zFZW~CP#rgH()Gf_{NW0S<(w*%Lm-EW`xIVEJkf3T@w7g00`O4G?Q?Qmw*}Gy3(7qY
z?wQhw8Ar4pbX!#IQoTa(297hKIxTbPHmN1k2^}hVzyKRDc5FD$k~^=!!zYAxINRaGk+F0`Y}nVkLC_zEQsZ
zxKcXw;9u6rKW6+e3{$TB?nI;1ps`9D5IT{xBoh8%W0zqoufeaKV7fHH3ZlZ*u#f<|
zf(nUHR-woys-wvYKXQenTvOt&RccRhg|ch(nGfBipQRA!q6pPb1jcgWBNRQ1DDET`m2A-=5jLD)yoeQp8CzZt
z14nye1j|vU*RsXODng`W^avVH;1#EWBC=a!s>Ywm$Q?<%^7pdWBcw{IY-%~dFq+j
z6^&QGX&~4=%s@46!ZKTLpp79&k`Ag-=KI81oz)oi+hg)unrO7Y!+X5i=uZc=IQe7Tz2_!i`04eFZZ$ZOz&}4LZ?)y$`&uF
zw}id7cc);h&PsJbVA$AgOlM?Lv2{7Mif{sq9t(!rND$_Ca}I8;K@}-Z@;cU_eLS1O
zFpN_7(|#Z9YQ?}p?Rm_=gu0+3sevseDeYHqM+gU402UkXRlYkx=>g3VIF1^LT@mMQ$-4VP
zck#s0h`VNNZ_MJ3T0CKkCu;Ftw|L)PAImC?W>tiuPm1eS4Rq)V~(sl79r0(c3|GdnXoQb7#0dAYNwY=Lg9?C1s#Fzro*=~;~Yso
z48CR4@jW?l0#+wtGBcmZs+(?SY8TI;^~FR+nkLOMVCG%7I7F9w=jrlp-*nfXo?>_O
zZlgR4#5P#BNyS~*GB6Cy9719>P{#nK1Z|3tEK#RpC`&D+gu74{Wgu1pqq>bFhEf=z
zWswlTEPN)^h0ee;1YBt_*bBKW)DGq3PtoFM#=6seU^P!7&Fn;27CL}53@1=+<3_B%
z#tp}ZgY-JO*)p%^I96_Z^yct;ARxYnLTtbqkpP(4bH*Q^wU>>xe2}%|oH3T^9)El?
zXVN6)R?TKsoioJjnddjZvibbhFK>BFf)fi|4KijF1@qd)hQ
zqNY$N=w&_sgmaQV2BOQ5p>TN@>NqZXzfguXrk-QhrardRJHla+DOUvn6>1a&rdubEp517
z+VFmJ%<7q_pU#!M>toiu@w3r_<>7+mk%DS!=TtO*ML2&&B!A_+JA?dXGSd0W5MD9d
zBCdEZlbtT@B+Bo!TFm@2uo5T?z;%p?Xp0g2Z{b-Q6)^H$%oN;(wfoD)uWcqXaF?Pn
zp_~?ox3ivph(sc>Pp8YPu&&~ctozr2VC$Nat|_L|U(^;{+=JYRp$>m-RUG#Ox0rsP
z{Iq?bb1?j+!Pa)ez7?`HVZ<;DryjWUC>*r{&nd;VnS-9xjY0dap{Q2Z;5A)AH0Y%sw^&Oh+V
z15tbNbvu@|+~+dp?C#5(q7@C{iiVkr^^xqBaK-uw|93p!@_fhp`iAi}7dO1V0k^{0
zE${cu*gGYm^V93uEkP13uM{-ZTCe505GSb)zgbWde~3rjQv>)83TcqW^BsURiZ2P5
zy1r(U(=xw7IW1_qIo44L(p3JaQP2dKfQ4x|K_k~W66P54J&6Z2n~C!(zKUic4$vGh
zN&-|NB9%C0&ZH>|iecolcUgu62+r31Gh5pu_JiTA?Ng7;IEp78x#3uWA7T4JNjUiF
zb^AdkP5e%7r4Z-qdr?UX^mZ@d@Ahs)^5VJvhB_D3IcM@BovMYXS7nKrAGML6*_pbN
zq0<8p*$L`pRn5`nJJ{aU+Rg}esymX2RT5`W`g{0eQ$?$?AKD8p887XhYMd?J81Zb1
z*f&eU=1y;!!CAI+@y5BJx@jf45eCcPhF5_>;dYf61T6|3c5%QL~t+WrOLK%~
z=TaBD+$}ee#f6Q;3=;B4tmb^C({KraX@lBu2H!bvCKz}d@Cxo)&{egkL;FhwuVdzxX
zc6uS%dxh*fvI`5=ZBcgQ7^XKazzsIYR!iX91ks@xrGT8o4#~rSnUeZ~G@t4sTPAbW
z(m>;<$ofnC1z~n(pz*g^o5ps>>^Y#OVSDjJC-Lup`AF2h;<|l>qWU#V@BVi1YQqmV
zUEB1%Er0&}?7DWT;h8y980^FO
zBG8K`YQc&Wo9(rjS--(=TnP5l&9P6DnTOzDsyZCkarMR_V>g^(u=m9bUwkXLRS|yi
zJ`4RCAV}h{N5+~!Ei!yI)t-l?RLZsM(zVaw%S_32>I2#D8NH28I8-xODi&K&LS8G&cxp&I
z6&&{WVjloAtHbSlpf4cCt@1J%oUR46L0rk;L0EbFI^d+yFb0+me*!j)G=wnNq(wIs
zu~dX=<@@RxN@+-ldK!A^&r|EE24tUOe^{ed^dS&glE~ajz^a@5Rl-DL*e`}vW>!eEGL3-c=mB!G7kxyTn6eKfg
z{yqzOpQe45leR9%lBwRPjs=#2PRooPi%>}(bPM#y+(8O#NIJy;<*ZE58r$Ny12{?cROv)|L=JZtbKD$_mxr;vK~QLeYm51rfys
zGD^e+GNOss=`Kl?Sj%GW%5gh9y#jDCCyRZ1bp{hUD|zzo3X#_hjX+lxkTZ#4;Pl
zINiU7sM?M_2L5A#0db59U}H<*c>fvk=j_KZ7+QKyLKYr5MxrmNc&uY_{y0Nrpe|@6
z6G;_+u(GnTF`2t(_8c3*dUub~26)B*9N|&Cu5npl=gc7XiufF{7teXiCOy&eHR19#k@AM9cip%-R#0;B*$dBJeE#>I
z|4DA~TtW3z)i-M1sfiS97#CtiCF9n)!qQ3e+s?O~k+Rh{3fGJ`#R@zZ&t5ot@#)u}
zo_yl%C*OK7
zhgH|Au6m>ud$CudWXUg#=0Y#l=e%p@vpILc_~@)_Y1CC6c2(c>ESVJEw!USZ%Ad~v
zX3@Vbij-}O;hr_-DV{j~=Bclq!ZRnU-mZD8Mq1MxUfLYcdD6IgO?zJ!_3%6P9j4
zeRdzlte88z%V&Yo2~v@Qv?PV51bxjK1PYcIhV0h+sHd%51KG*)laUtLKf>e0pHalH
zoG`E*mYD{+qa*Zx#ZN-W+4%5*Sx*ytxZ#BZd7a}+~MCgzhx$bVbO-k6;tI?P2XsHr)_GJ
zRJIPtSDZiI@`;V}RK)5wPaF+QPY;Zc
z;`q_jTOWPxJMj#4?IS2oIhrU*0>Nab?%lZV5`c
zaDZ-|xm-N;^pyv(TjtQCv%(R&_1w(z%_#8x$onT|gzlIu0QR)X28EM$*Ns6^EhYT0WD5^F;(ToE5
zR4{~s3(n6^Vf=GdOmkyKKLZJIMV6;kE;NX`a$%ah^RXfl5qBqH`=leMXazh*B(oMaj#_1cKP`!<(A
zLtl#Wv}yS)?J~XRZklEE@+5f3?mJj`%D}=C9RF~++j64m(sQUGqg;9yPF-&%n@T{o
zh|t@x(4Dk;)Io@x5>zzQKoXoi$tM6?JXMt-$r~UMxY!nzF6iX?TEF5>ssou;p4@8z
z?5fslWv0h=@iIF0nfrjU-&JKijkJ>nKTZn
z(If}6i)7q#_
zX%+vi21E)8QmJr#|LI}a+3S)(l_&%Q)zwIh0ectQ%MQOw4H($sxZ*a?L?aTZ0LKS$
z5s7Lnwqimy9nFqi%&)q|AtLuxT;6+}{gROmYk$UmGGDDFG5auufgIAuH2jXL9Z0eCEgTR%iGz
z<9?G3hMFQ${|izY#ZRB+c{*&yI;~+wu{wHNgMX;&bTzI#QyRfKeb5
zB7iet#K=IN5@1iuQb)z-NVQ?LS+_9r38;%p4Ab
z0gWr=y3&t;)dvfr`Ub=15^n#O`nj#f60V&
zjGQpa{DXaESg&RUf@RoGQ5GB-CR`5m_5VF4Lf(SW&YXW3RUW9rYc-?SkpZte@(a8dNVIgm#Z>kG|`;4&mX0GSro(p?^e_zyDI<{-xU^H!vW#ylL
z?v>{zPDHY*&I#C@=kQEa%-T!FT4E;Gtf?R>RD^{J>|fakFXS0vEBHmsntc;WKugTw
zxm5XP?HjeyimefEBiqpacF9{MU=bO%J0^?Ce8U8$^J)6Xo2p8ud=$NrYl7
zsP)ou8lOR>X~PKz&FCu&`U*k=If^yIMr1f#mPl(qr-N7pOzRpy>d<(L+Es1)Y1u5O?+U+fjfW)Ijv&;)?-S=qCVFYWLnx6#gL@^c
zqFqL_B6*@KiU*VWVD{DZ4Mxg#QL$?j>598Kcyc7vKY*ipPLusqrfz>lWf)yj{BK%u
zBQDUEanXavkmt>5e?RRRPIb0%swQJu6csB_{1EKgde6;`sdt*m$uK;Y>l`Y&k3=7?`;0^bM}}$KWZ-t+e_e{Ib$!Kb7Y@C@alm}
z6%(f--jz2THSm^mZ=1Jq*1{P})m-kfNbYjDMp%o!v1ek>r2k*-zp`$Ic3SGobP(U6
zB1;nub5zdJyaG;Fgo|dVS0v!!x@Cbh~5PbaXs)mBG0iV1{($D45b1ac+~W+Y&*SsML$KO>6k~Rw24p?m|3TzX
zD(q~bC7@)kB%npcl~yBhAF2{c$stiyA~{BM)9BrcO%2mV|dl
zJgqnAj>kGiuoVVA*?&UukRY*7_okPJ@-psy=4!(P>cG9FOm#6!0q^zHKmt(9O)!A|
zGi1pC4R=XdgmjVlNtIIwIKv%Mi`Y7f_l}@FY^4>7LD8GU=JktWNe;&Mlb`-+bn)
z&rBblcqUT1?aJu;o1~ns>y9o)l(BWi?wK%3rS;R+sZpt9OT@NS61FND{l=!{{C})Q
zG+M#{SUm)9QE>g-LX={JxqCR++$6z{6&Gi&VZn;(S1|J`i){jf)qbx$w-?;qhl81?#6ruWpj;`z2w&
z>RFS~Wamen3|O?ajb1!Q3z{*d4_%O_13nRn=lKz-O9`py-Z#z!Ic)-tXywxlxmsHl
z3&CgyL3qe{D-EGgOD|X2R2VCa1Sn5-u9V>85;n@#p%mAlu}Qrs0EACxT7hNDt(Hn(
zLhW@rcO$j0^_pM~k#|^O-f)&=q{}F;Es8o<;>Dq8
z*Db|!?t)8>sApB!vnt}LyWy@s*A&Zgoj?1^+4E0-`RR!#-aPZwGgIqtl&+4s3Zt&F
zunTMFQ{~^NdZ%i-=G#xdzvlnj@SitC9`HpUcsTsP!;uFbmCBFYa2>_UeD-SEIGZO4
zxt}sA?ZqZn^J@NDfs^8uO=et1^OB*n{%Nu?By-%)O~ONU%`MW%aygg(f&mlQ_329E$o{SxKt5UUYB2AvpwHu85-KL>G96*xue
z?W_1>>w+@n0~}8FMKWsA(Y}pWgy|E1_qj>n+SN9&m+)jN&+#9cM@&iY?0szG`E=D;Uc}VF3>67
zuGkj_r`LB|5}aPO=b%9XAjUSof`m+`L7&i4YDieRfdTrstSD$SoWw3&QP9^-btOdy
z+;r3!CT(JvO3$zY2=6mh)p*AuGs+jyrO)zIw^e1oA#9d#Uri9$WL?}O>)i}|bdcJC
z7BH?1hS-o23n7+9Q|UGr70sBV(9%D#8e`2FBO2s
zMd=Uyq<;WT@9E
zL1mjNCF}zWhNZ`__!CIyRY4DW>18x3{t-noWThohar4OFKtIk@`!Wi`AAmvF+f-=^
zkqlufC(Tk+SPjN@VsPK283IR_1vR!8HaC*HVd1^7i6ko~xaFgVW&|(oi|vZIm)^B6
z7B<|dwJ;3RjI}5M64`#;7n`;=ZR1Ca(sImtCPyR@wtprt{nJD+weoeuDeEpbu*=;H
zQv*|Sruc1xsqVL+vq5&6(S3knnvLv+RJ8&?ssTEnLJ5gV#jp`ha#$#%lNS{UFoDI6
zdb`wJ;%xd7_0|Mp$>5ZT-~ybNz**KigcO)&Bss-6lby(gW${yN!{oe6^ZF$;#dz`O
z1nxNhBI^tKy0Lu)~T|tKFjpk
zl9ux~5Fj&X-hmXzda?{460J_L%;d0rD=L*%zl16q=Ht%;$b|
z`}@@OGSp_|$yqtsHJv--+>nNH5Q%s(H=iGMuu)gsK6`Oj3mUdAL96nc{R(!E)4!(O
zW8ZJk?qNuF+x~oZk9y6K3Kk34rpe8^Ten#JPmCLzvF#*Mh$L`|bPwzE9L7&%XuGJ@
z0yF+kRO>E-g@YL^lqnM1(b|t_p8HT(##=hFEBWXUGFRB5o|R$G%7|yx4R>uCbA?pX
zc%!s2<|?@8yx_!HKx|_H%&IqBtH~}=M|O#vvGz}c*?16{B{G>=0`bZUq(^fSA?G(@
zlej+>V3uB9x2Lu;dul6x3DT9ox2CbDTG0QO@OOI>Q7Vf5b87)A%BPdmnI{q6hJq&g
z$X3<;FMoh0pO1VNX3aLfUTBMC4@Z>xL&fCh@7QMIhvNUD+HNIZnh87cq-lgUK}
zKxVQfl2M(`vao)-ZyK_Rwt6HBBEGkeymjQt#>pd*WjiH%izKwDGKr(f`CFGs{+eD#
zl}WTcvXDv0Y5+Kx@fw;opF3+s=JUq
zM@2rD=D%ek6cH+;LVZ}MpSE1ZIYRaDgHPby;sa=NbQz-pX&qJS^;0)~O}>3!3*8^l
zE2?~3;MWm3SVs%pxGciA&tcqBi9&)V1S|M*l%UX8PP`1ub!pG4PF=3+AQqbv2+8$r
z(uDrHrRiRab~$i+(+5;OEeq1$dw7J5?u3psU-sz?TNK-Hh#5nx?rL)QndNXmaF0qpSt#xlyy+D9-Olk
zU@@*_ZN%0f2@NXxb4`ZD9UzqRRFdF=28^KiHi=UAG8rx8V9Vb8zKo3jgWgyr7dBF1
z5WU+lPnU1wL?T=kAr}cBAo%r-j2fsUukJ0jG^7Hh;^#o)#sVqUI#B65tdm=2YEe41
z+?1@kz?(Da>#Pf?JMi38gck_ak~Oj%Yn;4p4RO~_A{eF2Z1rw0;q^PHYo=;u(|hL{CQp5_{uFSnSo7Y=<&+A4YhcTUB~9WWU=}
zamd;U>X7JN*hddQmsupV7_ZoQq7>vgJs*TCm%q_r>SL85J)1Bnn?{
zz0sGa9`~=P^7*nQhe#XqQx^|Yss?SzE)jo(R6Hw{2GRSAYY_F~PYVvDNRqid0XS;3
zj&Vf^XAyO3U*jI;Iy2M3H^DEn{cAH;%5}OHJM$O0i=6ViO6wRY<0xxQoX_OhO}Bc{
znFGImj0S~agU^m#7ew1x=;k`J4ri;Ua=_7{O*zrKwcDieIKVp&U(8BAwql*yqItnl
za9|WNALD&a9N3W6ZLWubS*?Xx7ktdeR&ker>~na-s!X+C=&5nvZduJ=uzeb3cVe5m
zXK`oJ7sJ`7e-heOpMrBuaI%TcJ2B5rH7~vQu5Y?S=pD?rTfsE6mGj&;6@3J$x1G!B4WL7>fSSI~VxbC)ss*(EptXMd-<
zODLt?mDKLAeL)yr3LQe@;$3#NkBPl4!FS|`?GMF92P$cUsD46=K8C6K`A4TD~gLOv{hatw%HiHr3
zNH&|ikN?B}@SUH%J|*ww|A%kCVT)&}eN`(N#N+4+HLDyCijy5kq{l`h^n^a+OJ2=!
z6WdwUUY*H~R*xHT0=s;^I^5b6=jp4IV&Xl~Dfv5=J1EtJD4wBxikbOfSLdc2yB@b`
zJqXG}^g7k0af-VX*XkZR@PKWIqNx%GDbK0qo$jL?I+C|n+(J(@}HO
zoXe}6?2P19pKHUGrSp4V**jsLv`$q`*GmP>Gmf3H;-!67x!M+3+u3DI{(V@t81O;G8
z*4q;G?hAYO;q7C^Rp?34emZ5p$a!ub_K)Oy&b9r92YJcYoNt)rF}~yEmaLk3e70oM
zIp~nysG}_GD4WW?B1n$18OOF*W|6dH?QG`SsAGHBvHi-EQpaJ*v3Nz*wqAE^{YR9}X#6bL0|a@gY5U$h?z`rd6t8dMarxfHCd-~`
z?)%%D-1zmMTuo+Nepo~$-nTWC;PRiVm+j3q{>W)W`bTb_(%CGXXWy4${88DGeS+~X
z1QXI-P)xsw$6za^KX7_zFiEN=y*LN>bz21HHyToP$8;YJ)8+qR$l#rep6-vmzI@_d
zUrkD3(kp$d6nlTUbT%Cy-)Ts<^96FecII7b>vXiaE^Vg!a3o|qcK(nm&)}Fd^IGG2
z0iTm(9|leSl7}yY7RUOuzV18A(GK1|{A~nZT~sOs4~L#kJltnh+b`kvf#s>Zk{LH`
zUF|3E41}8xjADX9{w-x~U^`@W95vW%jGc~VZYet%0Gm06Pe?OqfVJ3GhKn
zi$8dFu(y9G&iuqdH)$^!zHK}-9M58B;RfMPH^?>y$Fq=)?|t+H&IEc#@R54)8Xl#8
zjyGBT5Ha`zD>|3f_Lmt<9%
z%gT>tEx(?%Jd#z7-8;_gSYB~7Z&etZUi0drS#>wE>VD$Nn>UyBmbv-bGGpH45Y3Y5dC?1b3S*im~YM%u9d7o`nygOZhtqIr*yuzd7bgQtBrK0
zfv5C3N{_l}mvKECz*_864){-xX72R&o($CPq|det4UAd`hiZH2&ZzsL+NR##z(8#?
zj*SY4B#{^fhvGJrr1`@qL`Q9>0)gRLeDv(e0M3}k7m54&{qpCIEC3Q57!|B`r|wle
zM05NeMQs1dmnike6#WTBq$0@2zLL*(+;oURKn4!6LwnQ(a~Df8aZ3z|WFV=|lBECw
z2KjRmw4Y1-E;aQwMYK6V{31mpI>?ZdEHa2sQ7)4cpvH2?kF!(8ag-4YvWSqCA{~go
zi#q>^zaWXG|C<5Rhx!)jgdL36v4c_m7F){67$0hMSY~qK!-Us`
z+=-o2!i-QWTT5qz^}i78rWJR*oTKC?R{LwFSWeYV2Oaw1z}LvJp?hK#c5l0FcPv&q
zd&Wv<&k)Ift(9+e@NsKT4{bc{=^3@Gu4DiDcR=%?qsvbY1+g=pP6?=nbpIJ9>Q&UM
zhI54KGqZ-igf?*
zBEWbCQQUz8ulBa??CS9yJlMrfR>L9QgV-M{AD1P{7B*({%_^ug!R(J%6
zzme`XQ?!eseH1Z7JxnR41)QPOa}+U@{W7KgfTA}kV#v#|`Umv$uP6#rbd91}ilP+J
zUO@4ul;SBx9+m72B$1Asp-Bc+qQed7umw7yl5I0)n=aXQHIeoju`MCY_g*BgX>lh-
zWs$7q;2_~F@R9Z5to37#7-xLh`hxZ4tQWGz`3nu$^ORFL!!7$6w=c@=`zg2W
zueqAPvDw0#KhEX(
zBmBo!BftD(2haOivgl(6eTDsw!^PKpT!n8Rf9#~M)_&~3heSVi;wzG$Sn-9!Ph9lX
zw|Nh~Gj-L(zAYs^@)-LB)yI|isLjXO_|D14OX)iY)N%)pLn{oXmkVAfIG6KM@ht<-
zZ@F#c493O9Qe8LL1yp-vsOmA}L9Rr*jluV@NU23aE5kBvhVFT}?%=~#u%|rZE
zo+-C$z?X59x|D?0B4TfE}y5=yf-QLjseGLDY;0^dnniQf=9BIMfmcc
zEUTWX_``>JP|OBr&@w9Qg!nvzfv)fX&`@ZMVnGoMG<
zMUcVxJRFp&=F%(NF<_4q<)YU}&F81&D&vk^ZQR*ARYdqot=G#^dtDXbms87i)ba{y
zxh|n)HJ80H%k{{)Y+&mn{OUUf_#{%@HS?6xrtI932o|#O8ab3%NH3I=@IuHfy<;FR
zDatOJXUT-&kn@#SLbmowY^ddlfHwtI#zY{MP`5V1*WCexuuj(#tauV&g>rvkpfx|L
rjBHBIw;MUZac*?RSondEeN(W%yzj+*th@#HAZaoB|<0`XF?HMgucQBXf*<26Wj1J3b^|
zLTWq0wrt}UZH&CJjaS}SiJi4xj*Vlllfi(%Kk}~jJteXCWfNFSJ`-oF`ZEJ)Z7J{V
ze^9@ES9NuDb$4}D_gsHb@#tmVkY&(^aqxWh&wq6%t~q6hRqPLWb@5_;#~w}$sa{l-
zQO|QBE?A2yh0)O0{WGPr7BiR|Ds3^zllz}
z6frqBm9?a`q!%cBSuLhyPK*+xSMako$!^KsvYR^AME1qBBqVb!;qaeaI7<~{1AJnr
z7`H+h&_Ilj;lzX(uEXeCDp~+fWO%N)2=F9^FB7eRCo?=xOaVNV;rU`3;OPu65L?9z
zz<6Jwn7Ijx7qj9b(FXVuhA$Vh0MBOl3NZ)pr3_yw<^sNq;j6?v!1Ect8oF~3yOU55
z>_lMUmN&1nFyn^tJQo
zB|&;|kiKpny)>xB9DO}#v0;H0Wx#a;buB3gNNI_J;Vlh*ndg5MD8w?5QI0Y?_+Vcu
zK+2;ig|DFhE+nWn2ELQ40&>J^s0ZUS&jMAz#q6A16Xe+OmLa
zQ-CXBYmiHvXN{TwJ-H!BZv-jZ7D(9)T=K}xaWw(g_61zEL0LP3vUUR3t_56m0j@Q>
zgIsN5Gw`(_-#qKpw`>vjtlYtIP7B9bIB-9F^;X~3maSXPQHR0oYRMMu;$G1)Z|(4@
zz?90_oFC()th4cwu*KoqZ&OPll@713WJ|TAuWD@EQgNuw*?@yQHvfo{nyZv(nw+
zl7v~19GN7HT3+J^v
z_~lW6N-C7*l|qTj;k5f)hkWNbk_iNoj#Ql;$@aGHu6Fl6QiOHs3jJ0-ljeq(EIqFv
zR^(E)weKS-fZ2GG1}`tVj1(q;V0#P{(a-fsY82hZQ%8cCzG-TwXAG8l8*(fGP-hd!
zGI&d22Z`I~V%iW{NoP-|#|zR|0+~tA23&5t4)NLJZSC^0MraNQSrn{ifs{xRKkJUPB1FgNIqZ4KgEXYpML4O?cRe=?l
zWQ!#Oc3wehu^<(|w+;dQC^i%9WsLh^yA7FfeUU@}Qdk>E_PLyPhfm^cDoImb(pX|I
zuNKKS%uo|RT8nX@xaS!QSy_pKcPS{JR$LSy%4U0B`+
z-(M=1b~>Rls?LlGOL5jNpuMC7Co3u1JzfFm^0-rbc7o~wCaN#H0z@Sz)n<||tba=@
z0>eSNDkqiCywH;KGOxt}CIo?(M(6HO_agcL_2p(oFcOZiO*LyS`fBb93-j{Ez*@6=
zdpbKE^0EkN6U89qbIc~B%#9%6BCVcD|;LVx;F*@Ix97=oM1vwF&CzOGX0)YF_&rT#+Ftfnyq
zt{k~j%{~Nr|9_tFf(ii-7C)XI5kieW+yNOj_!QT4GT2)l551jnL(l>fDLKJMZ+FZBZd~vekIA&ux53T
zsuguNQT5H5Ak{FR+Mr0~igZdhZwn`7z(Nu){B~88GK(#lA7a7l2reQ(XC(>kEc!xs
zNd5s!AEpV#mTu-h&*Jwp2%bg2Rv-$JLfl?EtEYyY*45=C6iRJc2*4bzt`14l>hQYk
z=pWP`;%avvB0s@e>Q>_DYTHlDD2#bHR72w1Nf~{YK8p43)f4`FtfJ4D=4|A9jo=UJrRSWcct!{$lc589&On
z80$|foemLf@t<-+T*8csi(fiz;7ldC61reX1w?dP|T
zS&Fc%YRVitnDSialbKUy%V6ts`<~qAx2+ysw0g>%+&8pl%DiYuGp!HFiujBRF-6YA
zaWQ$*i@8w!@m&*|*hx*&s3vJheOZ&qmQIfjEO713r#^c5<=;K_-0#2pAc;UTh0Hqr
z9s_*+?%aKU7v&pTf`60BlO~uZXh=^b5e)E@a)XLEeQzap>)^sk_L#K*ZJUZKp$Jd8IL`
z#WVSwDq>*ixy8eqE{2W^r5^|;e?-c;x{=M7Y~#Xa_L(-GvHYUpZ6J$Rwc-6WUwCn@
z-&Yb_Vxjfr_gAuZvD2W)oQ~*t&ht#E`Tj{i-?4hhuYv15`1j`1_zFwxRZW6Fap{yM
z;_kc@&_mR$=eK01>sg6@$hT>}+-CX^r
z-B#paj^oFGL6ur8z@fU;j`=@8_r?Rj9TOJy41%-XX?(qQfPKNH`NXD=6#Pr{SZ!HI
zEb!J58-2Uh($^38Tztp2|0AaKojK{9Iq6Pode!Rm+STa|5}`Ygi=ZE?P&+!gIVE07h%dvN;gDag3k?%Y>9tlBuqr4jTzF%!ZP~Hu4p3%+8$lk
zUp^&h9^7=#rUBoOvwzdLkVOyI=k5xuTAp{
zX){Sw*z#7D71WgjuFZhmY7!WsPr>=Dj>XBjuIyj$V-t;
zW1H^gc{{f>gFXs|cYlx1-65$-Cv!z?at@&=HdwQfA&Qj*+>azhs}qj~IvwsV3^7zd
zq4f>w%yeWiI9(ks$kPLf8jegjcH(J3aCENGuMZ~kSLm}1DK@=nLci#^VoIw&x$4BK
zlWQJXb43?%vi70cv#EovPiJ1%rObqIrs8Q0{ZqrU8UF_3}
z=!Jd8^A+4g$PFMa!oJ%9VKdVZ*`K@diYaM4Id9CEe_WWljRnSaDfGprHpF$o$L#_XA0`
z03<5%nlz#l49W>qXFG@7W!#`YW&$(!;yA
z@}JOmcW2U$@=*Fo$FJzmn~Uieor!%4axV%&M-F+ruCW4|C~2&iYvU}EpF!|12tGvc
z76Mj9A%*%}ot_TJv%Tc!P|RZDfCUX~_nl)485
zvS~G^i}DL*I@1+LeR~@1&@*eGwcAdSjSFpE!E9Z@pJleL09%8kHsA?9A=J6+u!<9v
zO+4M&Y^Iw%G0f|HX>OeD)!Z(++A4eag`)%Oi35r9dYeBUXdbd)JfJ{gc}&0{L=817
zQRix)F+UO-r5Y3_LM`;f-ju{CUBp27plwKeq51jdiz|kj$I{FE+H${8{ss9OPa@v3ElvfU}zAq!|x~)D)L%eOo?U@apV1OS0ntb@lx`rKwX^GL%!U3iieHN7-y0e{r
z;On9%5AE%$M&Y`D|9UhC+H|szkdzuvT9UMJ!$tFk@3_6A!trp0S2BC(>(2SIpb#aRL$@bavi!Q^BVc9`MAoCpVJf$bOc*
zx6QW2UIu`e<9spzqqH`ppGyGq1Q;5$fqM-P*Pz>?43u;9Y`KL!<%v;NGP0DYgX6pkrIL@Q2*}`$5+#*nxiPRlSG%dJ{^{AWFbZDuW)*UNW
zh5-K9v9hEPF=~$hcb&7{6Jy{$b9M}q0+Q5ybA4UemzQG{69T==o>PGH@D1T`n~d@L
z>?2yTNg20}Jg{BE@3T3AG
zb-zg&DlVb}$E*=ens2g3zL*5ZG1ewcE}yFi3{w&(rlfLKZf-8CIVYT>S-2#w5e=O}
zkKGfMywT0AQ6_S3{w7X1g|={>FnQqm#AH!1*QLg=KqK>;ON0hJVe7-NSs8Ql0pYe(
z@A*AX0K25`
z@wy0HI?4Ms-Fk0QsuyeDfwl3TW}gcV&-eFq-C_3~_PEIJk+_pSa&KHTTAMrsV8IDC
zedFHbK3w!txP$oIZEyfEyGp^mCE)7S!nvB=*X?nGy5t0M3eN5>7sTf*tdvxcgLRWS
zo1Sb#qSDjjV;5|s13u;G^)zCblFhr3V%gYwtP$E5yg6i1uq}dMkRd6&9!b&Tfm6Jm
zR!PwY=h1C#l8PL_^CyMZNzza|73sup-X$OKDO`8T-aQv3z$sOCr^hE50|vuOINTOV
zKajoX4l$22B7mn~en>hkyf35gAc*%4!SegOpPN=>g)YAml{VBc7G*nLc~xgPS$U#z
zpmMNksC~HGAGvN^xBjXrW>7O>N*>CXFxiGH{RL$c+2#I`Dq=e51&)ozqHgJSvIaKzh+JwIx=C-J6-q|(6@tE&zY-O9X4mOmLrQ9gV>(l`Kh6gIn=}@U8jD_YzJB1J4qS@xi`)Hs9plSe
z{hdC)v1r`bGp$+B+;FEqa_zXT_#16>Y0pG<<7D>k(d^xRSC2n?_gHrCgsHE0zR6Zk
znAS|1N=HqlZ?E>Zwfjw_W2SvzwWyfmmGiCk|1#R5bE!kE-^-kcwLPVt>0@TQuH&Lo
zhjvYvbN`Ql3a9iD{wUjo-ZrT#8r2nzv|l>t*AZgL!qjIQnc*jVuKYN`&a{Xo9
zhR?63bLPbB91n)Q$(AE+mo|d!CXJh?l}PUU>iS~N7zeze%jX1c9BTSO!-(l0oBWZ5
z(CquVqOX8GborNFbRyp=S+~i={ZzdeVL^!&UO!u35>lPc{X$(Tz}GLsk@U;8C7JO0
zReJg+gYum)CE)MqdBhD2H)(5B%9wX7i)uLKyPOI>8sT0=4cBFmR$ci1gD>&v;Oe;}
zim))|+>Yp1Tlc%beYC=*U04`6j#OAXu#AqLC|gH?bM9(aQb8ouvdLndv@9F7EE_S8kk@-(?Y-!`)G%hL
zo{X*@P)@}zI_rDx$Wup#Q!iGGCzbjam0c?IM^}v*tEM8O2YR1a<4-9Vk1Y7W95&|jR~HM$R|dR
z1DB-nc{pI#K+-i|kIRydEvN6uu!XZMLfHcK``
z6+8TU7bOGy8%sUl?C8LR)(VG5aMZNs#F~@E4;9m6)zS3jNAml^69zjc!n69TrnHeK
z*PmEFskOYXwOlbI43_y*S6tMO^!bxFjv1=_nyRUoq{$fDXpC)Sg+InN7PAJ@xoi4Z
zf83Jc@-cm0f918X=!ci~mra?>1I-hrl>SZELJcPq9!hvPvA^^)AzQWl6Ar+2h}%?#
zNx}TSU>>BMkCqrQG$~abfsOlLpbGoVhx}Uw+{dKw
zlFgg#VnxHY&5f9EH%LmjFoCOIIJ5Q1b7TY7sz6YUfK4deAmweb7qPn#ut|0Xu_2D@
zu8WBzU^vJQI%{@vIY1yT}X@Zy@*r!Cw(z
z2G7z~f~gz9WQR>ANn>Z}jNLAU*aRsc4=8Y{%#6Ytk89^-A-6>vz2)Fso1O
Rv<;_C3b}ClA;dE~{V%G#N%8;y
diff --git a/web/server.py b/web/server.py
index 348d958..3f70ed1 100644
--- a/web/server.py
+++ b/web/server.py
@@ -23,6 +23,7 @@ DATA_DIR = DATA_ROOT / "meetings"
RESULTS_MD_DIR = PROJECT_ROOT / "data" / "results" / "md"
RESULTS_JSON_DIR = PROJECT_ROOT / "data" / "results" / "json"
TEMPLATE_DIR = PROJECT_ROOT / "template"
+TEMPLATE_GUIDE_DIR = PROJECT_ROOT / "template_guides"
PROMPT_DIR = PROJECT_ROOT / "prompt" / "zh"
EXAMPLES_DIR = PROJECT_ROOT / "examples"
CONFIG_FILE = PROJECT_ROOT / "config.json"
@@ -32,6 +33,7 @@ DATA_DIR.mkdir(parents=True, exist_ok=True)
RESULTS_MD_DIR.mkdir(parents=True, exist_ok=True)
RESULTS_JSON_DIR.mkdir(parents=True, exist_ok=True)
FRONTEND_ASSETS_DIR.mkdir(parents=True, exist_ok=True)
+TEMPLATE_GUIDE_DIR.mkdir(parents=True, exist_ok=True)
app = FastAPI(title="Meeting Summary Web")
app.mount("/assets", StaticFiles(directory=str(FRONTEND_ASSETS_DIR)), name="assets")
@@ -112,6 +114,48 @@ def _resolve_child(base_dir: Path, name: str) -> Path:
return target
+def _guide_path(template_name: str) -> Path:
+ return _resolve_child(TEMPLATE_GUIDE_DIR, template_name)
+
+
+def _collect_llm_content(client, model, system_prompt: str, user_prompt: str, max_token: int = 64000) -> str:
+ content = []
+ for chunk_type, chunk_content in _llm_stream(client, model, system_prompt, user_prompt, max_token=max_token):
+ if chunk_type == "content" and chunk_content:
+ content.append(str(chunk_content))
+ return "".join(content).strip()
+
+
+def _parse_template_guide(template_name: str, template_content: str, cfg: dict | None = None) -> str:
+ prompt = load_prompt("templatet_parser", "zh")
+ config = cfg or _load_config()
+ client = _get_llm_client(config)
+ system_prompt = prompt["system"]["role"] + prompt["mode_contracts"]["parse_template_requirements"]
+ user_prompt = prompt["user_template"]["template_input"].format(
+ template_name=template_name,
+ template_content=template_content,
+ )
+ return _collect_llm_content(client, config["model_name"], system_prompt, user_prompt)
+
+
+def _ensure_template_guide(template_name: str, *, force: bool = False, cfg: dict | None = None) -> str:
+ template_path = _resolve_child(TEMPLATE_DIR, template_name)
+ if not template_path.exists():
+ raise HTTPException(404, f"Template not found: {template_name}")
+
+ guide_path = _guide_path(template_name)
+ if guide_path.exists() and not force:
+ return guide_path.read_text(encoding="utf-8")
+
+ guide_content = _parse_template_guide(
+ template_name=template_name,
+ template_content=template_path.read_text(encoding="utf-8"),
+ cfg=cfg,
+ )
+ guide_path.write_text(guide_content, encoding="utf-8")
+ return guide_content
+
+
def _find_transcript_file(meeting_id: str) -> Path | None:
mdir = DATA_DIR / meeting_id
for ext in (".txt", ".md"):
@@ -201,25 +245,27 @@ async def file_tree():
)
tree["children"].append(branch)
- def _build_flat_branch(label, base_dir, prefix, suffixes):
+ def _build_flat_branch(label, base_dir, prefix, suffixes, delete_mode=None):
branch = {"name": label, "type": "folder", "children": []}
if base_dir.exists():
for f in sorted(base_dir.iterdir()):
if f.is_file() and f.suffix in suffixes:
- branch["children"].append(
- {
- "name": f.name,
- "type": "file",
- "path": f"{prefix}/{f.name}",
- }
- )
+ node = {
+ "name": f.name,
+ "type": "file",
+ "path": f"{prefix}/{f.name}",
+ }
+ if delete_mode:
+ node["delete_mode"] = delete_mode
+ branch["children"].append(node)
tree["children"].append(branch)
_build_branch("会议原文", DATA_DIR, "meetings", "meeting")
_build_branch("会议结果", RESULTS_MD_DIR, "results_md", "results")
_build_branch("结构化主题", RESULTS_JSON_DIR, "results_json", "results")
_build_flat_branch("提示词", PROMPT_DIR, "prompts", {".yaml", ".yml"})
- _build_flat_branch("模板", TEMPLATE_DIR, "templates", {".md"})
+ _build_flat_branch("模板", TEMPLATE_DIR, "templates", {".md"}, delete_mode="template")
+ _build_flat_branch("模板说明", TEMPLATE_GUIDE_DIR, "template_guides", {".md"})
return tree
@@ -341,7 +387,7 @@ async def list_templates():
if TEMPLATE_DIR.exists():
for f in sorted(TEMPLATE_DIR.iterdir()):
if f.is_file() and f.suffix == ".md":
- templates.append({"name": f.name})
+ templates.append({"name": f.name, "has_guide": _guide_path(f.name).exists()})
return templates
@@ -350,7 +396,11 @@ async def get_template(name: str):
fp = _resolve_child(TEMPLATE_DIR, name)
if not fp.exists():
raise HTTPException(404, f"Template not found: {name}")
- return {"name": name, "content": fp.read_text(encoding="utf-8")}
+ return {
+ "name": name,
+ "content": fp.read_text(encoding="utf-8"),
+ "has_guide": _guide_path(name).exists(),
+ }
@app.put("/api/templates/{name}")
@@ -362,6 +412,49 @@ async def save_template(name: str, payload: dict):
return {"ok": True}
+@app.delete("/api/templates/{name}")
+async def delete_template(name: str):
+ template_path = _resolve_child(TEMPLATE_DIR, name)
+ if not template_path.exists():
+ raise HTTPException(404, f"Template not found: {name}")
+ template_path.unlink()
+
+ guide_path = _guide_path(name)
+ if guide_path.exists():
+ guide_path.unlink()
+ return {"ok": True}
+
+
+@app.get("/api/templates/{name}/guide")
+async def get_template_guide(name: str):
+ template_path = _resolve_child(TEMPLATE_DIR, name)
+ if not template_path.exists():
+ raise HTTPException(404, f"Template not found: {name}")
+ guide_path = _guide_path(name)
+ if not guide_path.exists():
+ raise HTTPException(404, f"Template guide not found: {name}")
+ content = guide_path.read_text(encoding="utf-8")
+ return {"name": name, "content": content}
+
+
+@app.put("/api/templates/{name}/guide")
+async def save_template_guide(name: str, payload: dict):
+ content = payload.get("content")
+ if content is None:
+ raise HTTPException(400, "Missing content field")
+ template_path = _resolve_child(TEMPLATE_DIR, name)
+ if not template_path.exists():
+ raise HTTPException(404, f"Template not found: {name}")
+ _guide_path(name).write_text(content, encoding="utf-8")
+ return {"ok": True}
+
+
+@app.post("/api/templates/{name}/guide/reparse")
+async def reparse_template_guide(name: str):
+ content = _ensure_template_guide(name, force=True)
+ return {"name": name, "content": content}
+
+
@app.get("/api/prompts")
async def list_prompts():
prompts = []
@@ -427,6 +520,7 @@ async def process_meeting(meeting_id: str, request: Request, template_name: str
if not template_path.exists():
raise HTTPException(404, f"Template not found: {template_name}")
template_content = template_path.read_text(encoding="utf-8")
+ template_guide = _ensure_template_guide(template_name)
prompt = load_prompt("meeting_summary", "zh")
cfg = _load_config()
@@ -466,6 +560,8 @@ async def process_meeting(meeting_id: str, request: Request, template_name: str
system_prompt = prompt["system"]["role"] + prompt["mode_contracts"]["data_summary"].format(
template=template_content
)
+ if template_guide:
+ system_prompt += f"\n\n模板使用说明:\n{template_guide}"
user_prompt = prompt["user_template"]["article_summary"].format(
article=transcript,
sub_topices=sub_topics,