Browse Source
客户端
- 首页标题「语音记录助手」→「语音纪要」,去掉顶栏右侧的算力摘要
- 「生成」弹窗:去掉录音语言(传空,交给识别服务自动判断);「目标语言」改为「翻译」,
默认「默认」= 不翻译、总结跟随录音语言,选了语言 = 总结/概览用该语言输出;
新增「总结模型」一行(echomeet_getsummaryservices 拉列表,记住上次选择,summary_svc_id 带进 starttask/summary)
- 重新总结选「默认」时传 tolanguage=auto(该接口空串历来表示沿用上次语言)
- 纪要详情在思维导图右边加「PPT」页签:示意图 + 「生成 PPT」按钮,点了提示即将上线
服务端(echomeet)
- 转写不再翻译:TranslateProcess 只做 Translate=Original;StartTask 不再选翻译服务
- 总结输出语言由 summaryLanguageInstruction 追加在模板 system prompt 末尾(空=跟随转写语言)
- 模板解析 ResolveSummaryTemplate:有目标语言先找该语种版本,否则用客户端选中的那条
- formlanguage / tolanguage 的 "auto" 与空串同义;Azure 批量转写不指定语言时带候选语种自动识别,被拒退回 en-US
- console「会议配置 → 会议总结」:默认模型之外可勾选「供用户选择的模型」,勾了即打开应用级「允许客户端选择」
⚠️ 行为变化:服务端部署后老版本 App 也不再翻译转写(原「英文录音→中文」现在是英文转写 + 中文总结)。
验证:dart analyze 无 error;flutter test 224 全过;go build/vet 通过,echomeet/console 测试通过;
admin npm run build 通过。已部署测试服(app/console/admin),release 包装到 ALN-AL80。
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
main
21 changed files with 657 additions and 224 deletions
@ -0,0 +1,149 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
/// 纪要详情的「PPT」页签(2026-09-26 先上界面)。 |
||||
|
/// |
||||
|
/// 目前只有展示:一张示意图 + 「生成 PPT」按钮,点了提示功能即将上线。 |
||||
|
/// 真正接生成服务时,把 [_onGenerate] 换成调接口、再按状态显示进度/预览即可,页签位置不用动。 |
||||
|
class PptTab extends StatelessWidget { |
||||
|
const PptTab({super.key}); |
||||
|
|
||||
|
void _onGenerate() { |
||||
|
Get.snackbar( |
||||
|
'meetingPpt'.tr, |
||||
|
'meetingPptComingSoon'.tr, |
||||
|
snackPosition: SnackPosition.TOP, |
||||
|
duration: const Duration(seconds: 2), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDark = Theme.of(context).brightness == Brightness.dark; |
||||
|
final Color sub = isDark ? Colors.grey[400]! : Colors.grey[600]!; |
||||
|
return Center( |
||||
|
child: SingleChildScrollView( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.w), |
||||
|
child: Column( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: [ |
||||
|
_preview(isDark), |
||||
|
24.verticalSpace, |
||||
|
Text( |
||||
|
'meetingPptTitle'.tr, |
||||
|
style: TextStyle( |
||||
|
fontSize: 17.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isDark ? Colors.white : Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
8.verticalSpace, |
||||
|
Text( |
||||
|
'meetingPptDesc'.tr, |
||||
|
textAlign: TextAlign.center, |
||||
|
style: TextStyle(fontSize: 13.sp, color: sub, height: 1.5), |
||||
|
), |
||||
|
28.verticalSpace, |
||||
|
GestureDetector( |
||||
|
onTap: _onGenerate, |
||||
|
child: Container( |
||||
|
height: 46.w, |
||||
|
padding: EdgeInsets.symmetric(horizontal: 40.w), |
||||
|
alignment: Alignment.center, |
||||
|
decoration: BoxDecoration( |
||||
|
color: isDark ? Colors.white : Colors.black, |
||||
|
borderRadius: BorderRadius.circular(12.r), |
||||
|
), |
||||
|
child: Row( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: [ |
||||
|
Icon(Icons.auto_awesome, |
||||
|
size: 18.sp, |
||||
|
color: isDark ? Colors.black : Colors.white), |
||||
|
6.horizontalSpace, |
||||
|
Text( |
||||
|
'meetingPptGenerate'.tr, |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
color: isDark ? Colors.black : Colors.white, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/// 示意图:一张 16:9 的「幻灯片」,标题条 + 几行要点占位 |
||||
|
Widget _preview(bool isDark) { |
||||
|
final Color card = isDark ? const Color(0xFF2D2D2D) : Colors.white; |
||||
|
final Color bar = isDark ? Colors.grey[700]! : Colors.grey[200]!; |
||||
|
return Container( |
||||
|
width: 240.w, |
||||
|
height: 135.w, |
||||
|
padding: EdgeInsets.all(14.w), |
||||
|
decoration: BoxDecoration( |
||||
|
color: card, |
||||
|
borderRadius: BorderRadius.circular(12.r), |
||||
|
boxShadow: [ |
||||
|
BoxShadow( |
||||
|
color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.08), |
||||
|
blurRadius: 16, |
||||
|
offset: const Offset(0, 6), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
flex: 3, |
||||
|
child: Column( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
Container( |
||||
|
width: 90.w, |
||||
|
height: 12.w, |
||||
|
decoration: BoxDecoration( |
||||
|
gradient: const LinearGradient( |
||||
|
colors: [Color(0xFFAB47BC), Color(0xFFEC407A)], |
||||
|
), |
||||
|
borderRadius: BorderRadius.circular(4.r), |
||||
|
), |
||||
|
), |
||||
|
14.verticalSpace, |
||||
|
for (final w in [110.0, 90.0, 100.0, 70.0]) ...[ |
||||
|
Container( |
||||
|
width: w.w, |
||||
|
height: 7.w, |
||||
|
decoration: BoxDecoration( |
||||
|
color: bar, |
||||
|
borderRadius: BorderRadius.circular(3.r), |
||||
|
), |
||||
|
), |
||||
|
8.verticalSpace, |
||||
|
], |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
Expanded( |
||||
|
flex: 2, |
||||
|
child: Container( |
||||
|
height: double.infinity, |
||||
|
decoration: BoxDecoration( |
||||
|
color: const Color(0xFFEC407A).withValues(alpha: 0.12), |
||||
|
borderRadius: BorderRadius.circular(8.r), |
||||
|
), |
||||
|
child: Icon(Icons.slideshow, |
||||
|
size: 36.sp, color: const Color(0xFFEC407A)), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
package echomeet |
||||
|
|
||||
|
import ( |
||||
|
"strings" |
||||
|
"testing" |
||||
|
) |
||||
|
|
||||
|
// 客户端「自动 / 不翻译」的两种写法都要归一成空串
|
||||
|
func TestNormalizeAutoLang(t *testing.T) { |
||||
|
for in, want := range map[string]string{"": "", "auto": "", " AUTO ": "", "en-US": "en-US"} { |
||||
|
if got := normalizeAutoLang(in); got != want { |
||||
|
t.Errorf("normalizeAutoLang(%q)=%q want %q", in, got, want) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 不翻译 → 跟随转写语言;翻译 → 点名目标语种。两者绝不能混:
|
||||
|
// 不翻译时若写出了某个具体语种,英文录音会被总结成中文。
|
||||
|
func TestSummaryLanguageInstruction(t *testing.T) { |
||||
|
for _, to := range []string{"", "auto"} { |
||||
|
got := summaryLanguageInstruction(to) |
||||
|
if !strings.Contains(got, "same language as the transcript") { |
||||
|
t.Errorf("to=%q 应跟随转写语言,得到 %q", to, got) |
||||
|
} |
||||
|
} |
||||
|
got := summaryLanguageInstruction("ja-JP") |
||||
|
if !strings.Contains(got, "Japanese (ja-JP)") { |
||||
|
t.Errorf("ja-JP 应点名日语,得到 %q", got) |
||||
|
} |
||||
|
if got := summaryLanguageInstruction("xx-YY"); !strings.Contains(got, "xx-YY") { |
||||
|
t.Errorf("表外语种应原样写码,得到 %q", got) |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue