49 |
验证分析仪表盘
50 |
51 | {/* 裁决结果 */}
52 |
53 |
54 |
55 |
56 | {verdictStyle.label}
57 |
58 |
序列: {verificationResult.sequence}
59 |
60 |
61 |
62 | {factualityScore.toFixed(0)}%
63 |
64 |
事实性得分
65 |
66 |
67 |
68 |
69 | {/* 简化的注意力分析 */}
70 |
71 |
系统提示注意力检测
72 | {verificationResult.verdict_details.number_sequence_started && (
73 |
74 |
75 | 🔢 检测到数字序列开始
76 |
77 |
78 | )}
79 |
80 |
81 |
threshold ? '#10b981' : '#ef4444'
83 | }}>
84 | {maxSystemAttention.toFixed(1)}%
85 |
86 |
最大系统提示注意力
87 |
88 | 阈值:
89 | {threshold.toFixed(0)}%
90 |
91 | {maxSystemAttention > threshold ? (
92 |
93 | ✓ 高于阈值 - 非幻觉
94 |
95 | ) : (
96 |
97 | ✗ 低于阈值 - 检测到幻觉
98 |
99 | )}
100 |
101 |
102 |
103 |
104 | {/* 详细说明 */}
105 |
106 |
简化算法说明
107 |
108 | 本系统使用简化的注意力峰值检测算法:
109 |
110 |
111 | - • 检测时机:当数字序列开始时(第一个包含数字的token)
112 | - • 检测方法:计算系统提示部分(token 5 到系统提示结束)的最大注意力值
113 | - • 非幻觉:最大注意力 > {threshold.toFixed(0)}%
114 | - • 幻觉:最大注意力 ≤ {threshold.toFixed(0)}%
115 |
116 |
117 | 注:忽略前5个token(通常有高注意力峰值),只分析对系统提示的注意力模式。
118 |
119 |
120 |
121 | );
122 | }
--------------------------------------------------------------------------------
/live-audio/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './app/**/*.{js,ts,jsx,tsx,mdx}',
8 | './src/**/*.{js,ts,jsx,tsx,mdx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | background: "hsl(var(--background))",
22 | foreground: "hsl(var(--foreground))",
23 | },
24 | keyframes: {
25 | "accordion-down": {
26 | from: { height: 0 },
27 | to: { height: "var(--radix-accordion-content-height)" },
28 | },
29 | "accordion-up": {
30 | from: { height: "var(--radix-accordion-content-height)" },
31 | to: { height: 0 },
32 | },
33 | "fade-in": {
34 | '0%': { opacity: 0 },
35 | '100%': { opacity: 1 },
36 | },
37 | "slide-in": {
38 | '0%': { transform: 'translateY(5px)', opacity: 0 },
39 | '100%': { transform: 'translateY(0)', opacity: 1 },
40 | }
41 | },
42 | animation: {
43 | "accordion-down": "accordion-down 0.2s ease-out",
44 | "accordion-up": "accordion-up 0.2s ease-out",
45 | "fade-in": "fade-in 0.3s ease-out",
46 | "slide-in": "slide-in 0.3s ease-out",
47 | },
48 | typography: (theme) => ({
49 | DEFAULT: {
50 | css: {
51 | '--tw-prose-body': theme('colors.gray.900'),
52 | '--tw-prose-headings': theme('colors.gray.900'),
53 | '--tw-prose-links': theme('colors.blue.600'),
54 | '--tw-prose-code': theme('colors.gray.900'),
55 | maxWidth: 'none',
56 | color: 'var(--tw-prose-body)',
57 | fontSize: '1rem',
58 | lineHeight: '1.75',
59 | p: {
60 | marginTop: '1em',
61 | marginBottom: '1em',
62 | fontSize: '1rem',
63 | '&:first-child': {
64 | marginTop: 0,
65 | },
66 | '&:last-child': {
67 | marginBottom: 0,
68 | },
69 | },
70 | 'ul, ol': {
71 | paddingLeft: '1.5em',
72 | marginTop: '0.5em',
73 | marginBottom: '0.5em',
74 | },
75 | li: {
76 | marginTop: '0.25em',
77 | marginBottom: '0.25em',
78 | fontSize: '1rem',
79 | lineHeight: '1.5',
80 | p: {
81 | marginTop: '0.375em',
82 | marginBottom: '0.375em',
83 | },
84 | },
85 | 'h1, h2, h3, h4': {
86 | color: 'var(--tw-prose-headings)',
87 | marginTop: '1.5em',
88 | marginBottom: '0.5em',
89 | fontSize: '1.25rem',
90 | fontWeight: '600',
91 | lineHeight: '1.3',
92 | '&:first-child': {
93 | marginTop: 0,
94 | },
95 | },
96 | pre: {
97 | margin: '0.5em 0',
98 | padding: '0.5em',
99 | backgroundColor: 'transparent',
100 | borderRadius: '0.375rem',
101 | fontSize: '0.875rem',
102 | lineHeight: '1.5',
103 | overflowX: 'auto',
104 | },
105 | code: {
106 | color: 'var(--tw-prose-code)',
107 | backgroundColor: theme('colors.gray.100'),
108 | padding: '0.2em 0.4em',
109 | borderRadius: '0.25rem',
110 | fontSize: '0.875rem',
111 | fontWeight: '400',
112 | },
113 | 'pre code': {
114 | backgroundColor: 'transparent',
115 | padding: 0,
116 | fontSize: '0.875rem',
117 | color: 'inherit',
118 | fontWeight: '400',
119 | },
120 | blockquote: {
121 | borderLeftWidth: '4px',
122 | borderLeftColor: theme('colors.gray.200'),
123 | paddingLeft: '1em',
124 | fontStyle: 'italic',
125 | marginTop: '1em',
126 | marginBottom: '1em',
127 | fontSize: '1rem',
128 | },
129 | hr: {
130 | marginTop: '2em',
131 | marginBottom: '2em',
132 | },
133 | a: {
134 | color: 'var(--tw-prose-links)',
135 | textDecoration: 'underline',
136 | '&:hover': {
137 | color: theme('colors.blue.700'),
138 | },
139 | },
140 | table: {
141 | width: '100%',
142 | marginTop: '1em',
143 | marginBottom: '1em',
144 | borderCollapse: 'collapse',
145 | fontSize: '0.875rem',
146 | lineHeight: '1.5',
147 | },
148 | 'th, td': {
149 | padding: '0.5em',
150 | borderWidth: '1px',
151 | borderColor: theme('colors.gray.200'),
152 | },
153 | },
154 | },
155 | }),
156 | },
157 | },
158 | plugins: [
159 | require('@tailwindcss/typography'),
160 | require("tailwindcss-animate")
161 | ],
162 | }
163 |
--------------------------------------------------------------------------------
/werewolf-live-audio/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './app/**/*.{js,ts,jsx,tsx,mdx}',
8 | './src/**/*.{js,ts,jsx,tsx,mdx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | background: "hsl(var(--background))",
22 | foreground: "hsl(var(--foreground))",
23 | },
24 | keyframes: {
25 | "accordion-down": {
26 | from: { height: 0 },
27 | to: { height: "var(--radix-accordion-content-height)" },
28 | },
29 | "accordion-up": {
30 | from: { height: "var(--radix-accordion-content-height)" },
31 | to: { height: 0 },
32 | },
33 | "fade-in": {
34 | '0%': { opacity: 0 },
35 | '100%': { opacity: 1 },
36 | },
37 | "slide-in": {
38 | '0%': { transform: 'translateY(5px)', opacity: 0 },
39 | '100%': { transform: 'translateY(0)', opacity: 1 },
40 | }
41 | },
42 | animation: {
43 | "accordion-down": "accordion-down 0.2s ease-out",
44 | "accordion-up": "accordion-up 0.2s ease-out",
45 | "fade-in": "fade-in 0.3s ease-out",
46 | "slide-in": "slide-in 0.3s ease-out",
47 | },
48 | typography: (theme) => ({
49 | DEFAULT: {
50 | css: {
51 | '--tw-prose-body': theme('colors.gray.900'),
52 | '--tw-prose-headings': theme('colors.gray.900'),
53 | '--tw-prose-links': theme('colors.blue.600'),
54 | '--tw-prose-code': theme('colors.gray.900'),
55 | maxWidth: 'none',
56 | color: 'var(--tw-prose-body)',
57 | fontSize: '1rem',
58 | lineHeight: '1.75',
59 | p: {
60 | marginTop: '1em',
61 | marginBottom: '1em',
62 | fontSize: '1rem',
63 | '&:first-child': {
64 | marginTop: 0,
65 | },
66 | '&:last-child': {
67 | marginBottom: 0,
68 | },
69 | },
70 | 'ul, ol': {
71 | paddingLeft: '1.5em',
72 | marginTop: '0.5em',
73 | marginBottom: '0.5em',
74 | },
75 | li: {
76 | marginTop: '0.25em',
77 | marginBottom: '0.25em',
78 | fontSize: '1rem',
79 | lineHeight: '1.5',
80 | p: {
81 | marginTop: '0.375em',
82 | marginBottom: '0.375em',
83 | },
84 | },
85 | 'h1, h2, h3, h4': {
86 | color: 'var(--tw-prose-headings)',
87 | marginTop: '1.5em',
88 | marginBottom: '0.5em',
89 | fontSize: '1.25rem',
90 | fontWeight: '600',
91 | lineHeight: '1.3',
92 | '&:first-child': {
93 | marginTop: 0,
94 | },
95 | },
96 | pre: {
97 | margin: '0.5em 0',
98 | padding: '0.5em',
99 | backgroundColor: 'transparent',
100 | borderRadius: '0.375rem',
101 | fontSize: '0.875rem',
102 | lineHeight: '1.5',
103 | overflowX: 'auto',
104 | },
105 | code: {
106 | color: 'var(--tw-prose-code)',
107 | backgroundColor: theme('colors.gray.100'),
108 | padding: '0.2em 0.4em',
109 | borderRadius: '0.25rem',
110 | fontSize: '0.875rem',
111 | fontWeight: '400',
112 | },
113 | 'pre code': {
114 | backgroundColor: 'transparent',
115 | padding: 0,
116 | fontSize: '0.875rem',
117 | color: 'inherit',
118 | fontWeight: '400',
119 | },
120 | blockquote: {
121 | borderLeftWidth: '4px',
122 | borderLeftColor: theme('colors.gray.200'),
123 | paddingLeft: '1em',
124 | fontStyle: 'italic',
125 | marginTop: '1em',
126 | marginBottom: '1em',
127 | fontSize: '1rem',
128 | },
129 | hr: {
130 | marginTop: '2em',
131 | marginBottom: '2em',
132 | },
133 | a: {
134 | color: 'var(--tw-prose-links)',
135 | textDecoration: 'underline',
136 | '&:hover': {
137 | color: theme('colors.blue.700'),
138 | },
139 | },
140 | table: {
141 | width: '100%',
142 | marginTop: '1em',
143 | marginBottom: '1em',
144 | borderCollapse: 'collapse',
145 | fontSize: '0.875rem',
146 | lineHeight: '1.5',
147 | },
148 | 'th, td': {
149 | padding: '0.5em',
150 | borderWidth: '1px',
151 | borderColor: theme('colors.gray.200'),
152 | },
153 | },
154 | },
155 | }),
156 | },
157 | },
158 | plugins: [
159 | require('@tailwindcss/typography'),
160 | require("tailwindcss-animate")
161 | ],
162 | }
163 |
--------------------------------------------------------------------------------
/multimodal-ai-assistant/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './app/**/*.{js,ts,jsx,tsx,mdx}',
8 | './src/**/*.{js,ts,jsx,tsx,mdx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | background: "hsl(var(--background))",
22 | foreground: "hsl(var(--foreground))",
23 | },
24 | keyframes: {
25 | "accordion-down": {
26 | from: { height: 0 },
27 | to: { height: "var(--radix-accordion-content-height)" },
28 | },
29 | "accordion-up": {
30 | from: { height: "var(--radix-accordion-content-height)" },
31 | to: { height: 0 },
32 | },
33 | "fade-in": {
34 | '0%': { opacity: 0 },
35 | '100%': { opacity: 1 },
36 | },
37 | "slide-in": {
38 | '0%': { transform: 'translateY(5px)', opacity: 0 },
39 | '100%': { transform: 'translateY(0)', opacity: 1 },
40 | }
41 | },
42 | animation: {
43 | "accordion-down": "accordion-down 0.2s ease-out",
44 | "accordion-up": "accordion-up 0.2s ease-out",
45 | "fade-in": "fade-in 0.3s ease-out",
46 | "slide-in": "slide-in 0.3s ease-out",
47 | },
48 | typography: (theme) => ({
49 | DEFAULT: {
50 | css: {
51 | '--tw-prose-body': theme('colors.gray.900'),
52 | '--tw-prose-headings': theme('colors.gray.900'),
53 | '--tw-prose-links': theme('colors.blue.600'),
54 | '--tw-prose-code': theme('colors.gray.900'),
55 | maxWidth: 'none',
56 | color: 'var(--tw-prose-body)',
57 | fontSize: '1rem',
58 | lineHeight: '1.75',
59 | p: {
60 | marginTop: '1em',
61 | marginBottom: '1em',
62 | fontSize: '1rem',
63 | '&:first-child': {
64 | marginTop: 0,
65 | },
66 | '&:last-child': {
67 | marginBottom: 0,
68 | },
69 | },
70 | 'ul, ol': {
71 | paddingLeft: '1.5em',
72 | marginTop: '0.5em',
73 | marginBottom: '0.5em',
74 | },
75 | li: {
76 | marginTop: '0.25em',
77 | marginBottom: '0.25em',
78 | fontSize: '1rem',
79 | lineHeight: '1.5',
80 | p: {
81 | marginTop: '0.375em',
82 | marginBottom: '0.375em',
83 | },
84 | },
85 | 'h1, h2, h3, h4': {
86 | color: 'var(--tw-prose-headings)',
87 | marginTop: '1.5em',
88 | marginBottom: '0.5em',
89 | fontSize: '1.25rem',
90 | fontWeight: '600',
91 | lineHeight: '1.3',
92 | '&:first-child': {
93 | marginTop: 0,
94 | },
95 | },
96 | pre: {
97 | margin: '0.5em 0',
98 | padding: '0.5em',
99 | backgroundColor: 'transparent',
100 | borderRadius: '0.375rem',
101 | fontSize: '0.875rem',
102 | lineHeight: '1.5',
103 | overflowX: 'auto',
104 | },
105 | code: {
106 | color: 'var(--tw-prose-code)',
107 | backgroundColor: theme('colors.gray.100'),
108 | padding: '0.2em 0.4em',
109 | borderRadius: '0.25rem',
110 | fontSize: '0.875rem',
111 | fontWeight: '400',
112 | },
113 | 'pre code': {
114 | backgroundColor: 'transparent',
115 | padding: 0,
116 | fontSize: '0.875rem',
117 | color: 'inherit',
118 | fontWeight: '400',
119 | },
120 | blockquote: {
121 | borderLeftWidth: '4px',
122 | borderLeftColor: theme('colors.gray.200'),
123 | paddingLeft: '1em',
124 | fontStyle: 'italic',
125 | marginTop: '1em',
126 | marginBottom: '1em',
127 | fontSize: '1rem',
128 | },
129 | hr: {
130 | marginTop: '2em',
131 | marginBottom: '2em',
132 | },
133 | a: {
134 | color: 'var(--tw-prose-links)',
135 | textDecoration: 'underline',
136 | '&:hover': {
137 | color: theme('colors.blue.700'),
138 | },
139 | },
140 | table: {
141 | width: '100%',
142 | marginTop: '1em',
143 | marginBottom: '1em',
144 | borderCollapse: 'collapse',
145 | fontSize: '0.875rem',
146 | lineHeight: '1.5',
147 | },
148 | 'th, td': {
149 | padding: '0.5em',
150 | borderWidth: '1px',
151 | borderColor: theme('colors.gray.200'),
152 | },
153 | },
154 | },
155 | }),
156 | },
157 | },
158 | plugins: [
159 | require('@tailwindcss/typography'),
160 | require("tailwindcss-animate")
161 | ],
162 | }
163 |
--------------------------------------------------------------------------------
/live-audio/backend/utils/textProcessor.js:
--------------------------------------------------------------------------------
1 | const emojiRegex = require('emoji-regex');
2 |
3 | // Remove emoji from the sentence
4 | function removeEmoji(sentence) {
5 | const regex = emojiRegex();
6 | return sentence.replace(regex, ' ').trim();
7 | }
8 |
9 | // Convert markdown to plain text
10 | function markdownToText(markdown) {
11 | let text = markdown;
12 |
13 | // Remove links, keeping only the link text
14 | text = text.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1');
15 |
16 | // Remove headers
17 | text = text.replace(/^#+\s*/gm, '');
18 |
19 | // Remove bold and italic markers
20 | text = text.replace(/\*\*/g, '').replace(/\*/g, '')
21 | .replace(/__/g, '').replace(/_/g, '');
22 |
23 | // Remove blockquotes
24 | text = text.replace(/^>\s*/gm, '');
25 |
26 | // Remove horizontal rules
27 | text = text.replace(/[-*_]{3,}/g, '');
28 |
29 | // Remove list markers
30 | text = text.replace(/^[-*+]\s*/gm, '');
31 |
32 | // Remove code block markers
33 | text = text.replace(/```/g, '');
34 |
35 | return text.trim();
36 | }
37 |
38 | // Convert numbers to words
39 | function numberToWords(num) {
40 | const ones = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
41 | const tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
42 | const teens = ['ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'];
43 | const scales = ['', 'thousand', 'million', 'billion'];
44 |
45 | if (num === 0) return 'zero';
46 |
47 | function convertGroup(n) {
48 | let result = '';
49 |
50 | if (n >= 100) {
51 | result += ones[Math.floor(n / 100)] + ' hundred ';
52 | n %= 100;
53 | }
54 |
55 | if (n >= 20) {
56 | result += tens[Math.floor(n / 10)] + ' ';
57 | n %= 10;
58 | if (n > 0) {
59 | result += ones[n] + ' ';
60 | }
61 | } else if (n >= 10) {
62 | result += teens[n - 10] + ' ';
63 | } else if (n > 0) {
64 | result += ones[n] + ' ';
65 | }
66 |
67 | return result;
68 | }
69 |
70 | let result = '';
71 | let groupIndex = 0;
72 |
73 | while (num > 0) {
74 | const group = num % 1000;
75 | if (group !== 0) {
76 | result = convertGroup(group) + scales[groupIndex] + ' ' + result;
77 | }
78 | num = Math.floor(num / 1000);
79 | groupIndex++;
80 | }
81 |
82 | return result.trim();
83 | }
84 |
85 | // Pronounce special characters
86 | function pronounceSpecialCharacters(text, isCodeBlock = false) {
87 | const specialCharMap = {
88 | '@': 'at',
89 | '#': 'hash',
90 | '$': 'dollar',
91 | '%': 'percent',
92 | '^': 'caret',
93 | '&': 'ampersand',
94 | '*': 'asterisk',
95 | '_': 'underscore',
96 | '=': 'equals',
97 | '+': 'plus',
98 | '[': 'left square bracket',
99 | ']': 'right square bracket',
100 | '{': 'left curly brace',
101 | '}': 'right curly brace',
102 | '|': 'vertical bar',
103 | '\\': 'backslash',
104 | '<': 'less than',
105 | '>': 'greater than',
106 | '/': 'slash',
107 | '`': 'backtick',
108 | '~': 'tilde',
109 | };
110 |
111 | const punctuationMap = {
112 | '!': 'exclamation',
113 | '.': 'dot',
114 | ',': 'comma',
115 | '?': 'question mark',
116 | ';': 'semicolon',
117 | ':': 'colon',
118 | '"': 'double quote',
119 | "'": 'single quote',
120 | '-': 'minus',
121 | '(': 'left parenthesis',
122 | ')': 'right parenthesis',
123 | };
124 |
125 | let processedText = text;
126 |
127 | // Replace special characters
128 | Object.entries(specialCharMap).forEach(([char, pronunciation]) => {
129 | processedText = processedText.replace(new RegExp(char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), ` ${pronunciation} `);
130 | });
131 |
132 | // Replace punctuation if in code block
133 | if (isCodeBlock) {
134 | Object.entries(punctuationMap).forEach(([char, pronunciation]) => {
135 | processedText = processedText.replace(new RegExp(char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), ` ${pronunciation} `);
136 | });
137 | }
138 |
139 | return processedText;
140 | }
141 |
142 | // Pronounce numbers in text
143 | function pronounceNumbers(text, language) {
144 | if (language.startsWith('zh')) return text;
145 |
146 | return text.replace(/(\d+\.?\d*)/g, match => {
147 | const num = parseFloat(match);
148 | if (isNaN(num)) return match;
149 |
150 | if (match.includes('.')) {
151 | const [integer, decimal] = match.split('.');
152 | return `${numberToWords(parseInt(integer))} point ${decimal.split('').map(d => numberToWords(parseInt(d))).join(' ')}`;
153 | }
154 | return numberToWords(parseInt(match));
155 | });
156 | }
157 |
158 | // Remove emotional indicators
159 | function removeEmotions(text) {
160 | return text.replace(/\*[a-zA-Z0-9 -]*\*/g, '').trim();
161 | }
162 |
163 | // Process code blocks
164 | function pronounceCodeBlock(text) {
165 | return text.replace(/`([^`\n]+)`|```(?:[\s\S]*?)```/g, (match) => {
166 | const content = match.startsWith('```')
167 | ? match.slice(3, -3)
168 | : match.slice(1, -1);
169 | return pronounceSpecialCharacters(content, true);
170 | });
171 | }
172 |
173 | // Main preprocessing function
174 | function preprocessSentence(sentence, language = 'en') {
175 | let processed = sentence;
176 | processed = pronounceCodeBlock(processed);
177 | processed = markdownToText(processed);
178 | processed = pronounceNumbers(processed, language);
179 | processed = removeEmotions(processed);
180 | processed = pronounceSpecialCharacters(processed);
181 | processed = removeEmoji(processed);
182 | return processed.trim();
183 | }
184 |
185 | module.exports = {
186 | preprocessSentence
187 | };
188 |
--------------------------------------------------------------------------------
/multimodal-ai-assistant/backend/utils/textProcessor.js:
--------------------------------------------------------------------------------
1 | const emojiRegex = require('emoji-regex');
2 |
3 | // Remove emoji from the sentence
4 | function removeEmoji(sentence) {
5 | const regex = emojiRegex();
6 | return sentence.replace(regex, ' ').trim();
7 | }
8 |
9 | // Convert markdown to plain text
10 | function markdownToText(markdown) {
11 | let text = markdown;
12 |
13 | // Remove links, keeping only the link text
14 | text = text.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1');
15 |
16 | // Remove headers
17 | text = text.replace(/^#+\s*/gm, '');
18 |
19 | // Remove bold and italic markers
20 | text = text.replace(/\*\*/g, '').replace(/\*/g, '')
21 | .replace(/__/g, '').replace(/_/g, '');
22 |
23 | // Remove blockquotes
24 | text = text.replace(/^>\s*/gm, '');
25 |
26 | // Remove horizontal rules
27 | text = text.replace(/[-*_]{3,}/g, '');
28 |
29 | // Remove list markers
30 | text = text.replace(/^[-*+]\s*/gm, '');
31 |
32 | // Remove code block markers
33 | text = text.replace(/```/g, '');
34 |
35 | return text.trim();
36 | }
37 |
38 | // Convert numbers to words
39 | function numberToWords(num) {
40 | const ones = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
41 | const tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
42 | const teens = ['ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'];
43 | const scales = ['', 'thousand', 'million', 'billion'];
44 |
45 | if (num === 0) return 'zero';
46 |
47 | function convertGroup(n) {
48 | let result = '';
49 |
50 | if (n >= 100) {
51 | result += ones[Math.floor(n / 100)] + ' hundred ';
52 | n %= 100;
53 | }
54 |
55 | if (n >= 20) {
56 | result += tens[Math.floor(n / 10)] + ' ';
57 | n %= 10;
58 | if (n > 0) {
59 | result += ones[n] + ' ';
60 | }
61 | } else if (n >= 10) {
62 | result += teens[n - 10] + ' ';
63 | } else if (n > 0) {
64 | result += ones[n] + ' ';
65 | }
66 |
67 | return result;
68 | }
69 |
70 | let result = '';
71 | let groupIndex = 0;
72 |
73 | while (num > 0) {
74 | const group = num % 1000;
75 | if (group !== 0) {
76 | result = convertGroup(group) + scales[groupIndex] + ' ' + result;
77 | }
78 | num = Math.floor(num / 1000);
79 | groupIndex++;
80 | }
81 |
82 | return result.trim();
83 | }
84 |
85 | // Pronounce special characters
86 | function pronounceSpecialCharacters(text, isCodeBlock = false) {
87 | const specialCharMap = {
88 | '@': 'at',
89 | '#': 'hash',
90 | '$': 'dollar',
91 | '%': 'percent',
92 | '^': 'caret',
93 | '&': 'ampersand',
94 | '*': 'asterisk',
95 | '_': 'underscore',
96 | '=': 'equals',
97 | '+': 'plus',
98 | '[': 'left square bracket',
99 | ']': 'right square bracket',
100 | '{': 'left curly brace',
101 | '}': 'right curly brace',
102 | '|': 'vertical bar',
103 | '\\': 'backslash',
104 | '<': 'less than',
105 | '>': 'greater than',
106 | '/': 'slash',
107 | '`': 'backtick',
108 | '~': 'tilde',
109 | };
110 |
111 | const punctuationMap = {
112 | '!': 'exclamation',
113 | '.': 'dot',
114 | ',': 'comma',
115 | '?': 'question mark',
116 | ';': 'semicolon',
117 | ':': 'colon',
118 | '"': 'double quote',
119 | "'": 'single quote',
120 | '-': 'minus',
121 | '(': 'left parenthesis',
122 | ')': 'right parenthesis',
123 | };
124 |
125 | let processedText = text;
126 |
127 | // Replace special characters
128 | Object.entries(specialCharMap).forEach(([char, pronunciation]) => {
129 | processedText = processedText.replace(new RegExp(char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), ` ${pronunciation} `);
130 | });
131 |
132 | // Replace punctuation if in code block
133 | if (isCodeBlock) {
134 | Object.entries(punctuationMap).forEach(([char, pronunciation]) => {
135 | processedText = processedText.replace(new RegExp(char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), ` ${pronunciation} `);
136 | });
137 | }
138 |
139 | return processedText;
140 | }
141 |
142 | // Pronounce numbers in text
143 | function pronounceNumbers(text, language) {
144 | if (language.startsWith('zh')) return text;
145 |
146 | return text.replace(/(\d+\.?\d*)/g, match => {
147 | const num = parseFloat(match);
148 | if (isNaN(num)) return match;
149 |
150 | if (match.includes('.')) {
151 | const [integer, decimal] = match.split('.');
152 | return `${numberToWords(parseInt(integer))} point ${decimal.split('').map(d => numberToWords(parseInt(d))).join(' ')}`;
153 | }
154 | return numberToWords(parseInt(match));
155 | });
156 | }
157 |
158 | // Remove emotional indicators
159 | function removeEmotions(text) {
160 | return text.replace(/\*[a-zA-Z0-9 -]*\*/g, '').trim();
161 | }
162 |
163 | // Process code blocks
164 | function pronounceCodeBlock(text) {
165 | return text.replace(/`([^`\n]+)`|```(?:[\s\S]*?)```/g, (match) => {
166 | const content = match.startsWith('```')
167 | ? match.slice(3, -3)
168 | : match.slice(1, -1);
169 | return pronounceSpecialCharacters(content, true);
170 | });
171 | }
172 |
173 | // Main preprocessing function
174 | function preprocessSentence(sentence, language = 'en') {
175 | let processed = sentence;
176 | processed = pronounceCodeBlock(processed);
177 | processed = markdownToText(processed);
178 | processed = pronounceNumbers(processed, language);
179 | processed = removeEmotions(processed);
180 | processed = pronounceSpecialCharacters(processed);
181 | processed = removeEmoji(processed);
182 | return processed.trim();
183 | }
184 |
185 | module.exports = {
186 | preprocessSentence
187 | };
188 |
--------------------------------------------------------------------------------
/werewolf-live-audio/backend/utils/textProcessor.js:
--------------------------------------------------------------------------------
1 | const emojiRegex = require('emoji-regex');
2 |
3 | // Remove emoji from the sentence
4 | function removeEmoji(sentence) {
5 | const regex = emojiRegex();
6 | return sentence.replace(regex, ' ').trim();
7 | }
8 |
9 | // Convert markdown to plain text
10 | function markdownToText(markdown) {
11 | let text = markdown;
12 |
13 | // Remove links, keeping only the link text
14 | text = text.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1');
15 |
16 | // Remove headers
17 | text = text.replace(/^#+\s*/gm, '');
18 |
19 | // Remove bold and italic markers
20 | text = text.replace(/\*\*/g, '').replace(/\*/g, '')
21 | .replace(/__/g, '').replace(/_/g, '');
22 |
23 | // Remove blockquotes
24 | text = text.replace(/^>\s*/gm, '');
25 |
26 | // Remove horizontal rules
27 | text = text.replace(/[-*_]{3,}/g, '');
28 |
29 | // Remove list markers
30 | text = text.replace(/^[-*+]\s*/gm, '');
31 |
32 | // Remove code block markers
33 | text = text.replace(/```/g, '');
34 |
35 | return text.trim();
36 | }
37 |
38 | // Convert numbers to words
39 | function numberToWords(num) {
40 | const ones = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
41 | const tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
42 | const teens = ['ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'];
43 | const scales = ['', 'thousand', 'million', 'billion'];
44 |
45 | if (num === 0) return 'zero';
46 |
47 | function convertGroup(n) {
48 | let result = '';
49 |
50 | if (n >= 100) {
51 | result += ones[Math.floor(n / 100)] + ' hundred ';
52 | n %= 100;
53 | }
54 |
55 | if (n >= 20) {
56 | result += tens[Math.floor(n / 10)] + ' ';
57 | n %= 10;
58 | if (n > 0) {
59 | result += ones[n] + ' ';
60 | }
61 | } else if (n >= 10) {
62 | result += teens[n - 10] + ' ';
63 | } else if (n > 0) {
64 | result += ones[n] + ' ';
65 | }
66 |
67 | return result;
68 | }
69 |
70 | let result = '';
71 | let groupIndex = 0;
72 |
73 | while (num > 0) {
74 | const group = num % 1000;
75 | if (group !== 0) {
76 | result = convertGroup(group) + scales[groupIndex] + ' ' + result;
77 | }
78 | num = Math.floor(num / 1000);
79 | groupIndex++;
80 | }
81 |
82 | return result.trim();
83 | }
84 |
85 | // Pronounce special characters
86 | function pronounceSpecialCharacters(text, isCodeBlock = false) {
87 | const specialCharMap = {
88 | '@': 'at',
89 | '#': 'hash',
90 | '$': 'dollar',
91 | '%': 'percent',
92 | '^': 'caret',
93 | '&': 'ampersand',
94 | '*': 'asterisk',
95 | '_': 'underscore',
96 | '=': 'equals',
97 | '+': 'plus',
98 | '[': 'left square bracket',
99 | ']': 'right square bracket',
100 | '{': 'left curly brace',
101 | '}': 'right curly brace',
102 | '|': 'vertical bar',
103 | '\\': 'backslash',
104 | '<': 'less than',
105 | '>': 'greater than',
106 | '/': 'slash',
107 | '`': 'backtick',
108 | '~': 'tilde',
109 | };
110 |
111 | const punctuationMap = {
112 | '!': 'exclamation',
113 | '.': 'dot',
114 | ',': 'comma',
115 | '?': 'question mark',
116 | ';': 'semicolon',
117 | ':': 'colon',
118 | '"': 'double quote',
119 | "'": 'single quote',
120 | '-': 'minus',
121 | '(': 'left parenthesis',
122 | ')': 'right parenthesis',
123 | };
124 |
125 | let processedText = text;
126 |
127 | // Replace special characters
128 | Object.entries(specialCharMap).forEach(([char, pronunciation]) => {
129 | processedText = processedText.replace(new RegExp(char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), ` ${pronunciation} `);
130 | });
131 |
132 | // Replace punctuation if in code block
133 | if (isCodeBlock) {
134 | Object.entries(punctuationMap).forEach(([char, pronunciation]) => {
135 | processedText = processedText.replace(new RegExp(char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), ` ${pronunciation} `);
136 | });
137 | }
138 |
139 | return processedText;
140 | }
141 |
142 | // Pronounce numbers in text
143 | function pronounceNumbers(text, language) {
144 | if (language.startsWith('zh')) return text;
145 |
146 | return text.replace(/(\d+\.?\d*)/g, match => {
147 | const num = parseFloat(match);
148 | if (isNaN(num)) return match;
149 |
150 | if (match.includes('.')) {
151 | const [integer, decimal] = match.split('.');
152 | return `${numberToWords(parseInt(integer))} point ${decimal.split('').map(d => numberToWords(parseInt(d))).join(' ')}`;
153 | }
154 | return numberToWords(parseInt(match));
155 | });
156 | }
157 |
158 | // Remove emotional indicators
159 | function removeEmotions(text) {
160 | return text.replace(/\*[a-zA-Z0-9 -]*\*/g, '').trim();
161 | }
162 |
163 | // Process code blocks
164 | function pronounceCodeBlock(text) {
165 | return text.replace(/`([^`\n]+)`|```(?:[\s\S]*?)```/g, (match) => {
166 | const content = match.startsWith('```')
167 | ? match.slice(3, -3)
168 | : match.slice(1, -1);
169 | return pronounceSpecialCharacters(content, true);
170 | });
171 | }
172 |
173 | // Main preprocessing function
174 | function preprocessSentence(sentence, language = 'en') {
175 | let processed = sentence;
176 | processed = pronounceCodeBlock(processed);
177 | processed = markdownToText(processed);
178 | processed = pronounceNumbers(processed, language);
179 | processed = removeEmotions(processed);
180 | processed = pronounceSpecialCharacters(processed);
181 | processed = removeEmoji(processed);
182 | return processed.trim();
183 | }
184 |
185 | module.exports = {
186 | preprocessSentence
187 | };
188 |
--------------------------------------------------------------------------------
/deep-research/crawler.py:
--------------------------------------------------------------------------------
1 | from selenium import webdriver
2 | from bs4 import BeautifulSoup
3 | import markdownify
4 | from concurrent.futures import ThreadPoolExecutor
5 | from utils.content_processing import html_to_markdown, needs_visual_analysis
6 | import logging
7 | from typing import Optional, Dict
8 | import traceback
9 | from utils.cache_manager import CacheManager
10 |
11 | class WebCrawler:
12 | def __init__(self, config):
13 | self.config = config
14 | self.timeout = config.crawl_timeout
15 | self.max_threads = config.max_threads
16 | self._setup_logging()
17 |
18 | def gather_sources(self, plan):
19 | urls = self._get_urls_from_plan(plan)
20 | with ThreadPoolExecutor(max_workers=self.max_threads) as executor:
21 | results = list(executor.map(self.process_url, urls))
22 | return [r for r in results if r is not None]
23 |
24 | @staticmethod
25 | def process_url(url: str, cache_manager: Optional[CacheManager] = None) -> Optional[Dict]:
26 | """Process a single URL and return its content (process-safe version)"""
27 | # Check cache first
28 | if cache_manager:
29 | cached_content = cache_manager.get_crawled_content(url)
30 | if cached_content:
31 | logging.info(f"Using cached content for {url}")
32 | return cached_content
33 |
34 | driver = None
35 | try:
36 | # Create new driver for this process
37 | options = webdriver.ChromeOptions()
38 | options.add_argument("--headless")
39 | options.add_argument("--no-sandbox")
40 | options.add_argument("--disable-dev-shm-usage")
41 | driver = webdriver.Chrome(options=options)
42 |
43 | # Set reasonable timeout
44 | driver.implicitly_wait(30)
45 |
46 | try:
47 | driver.get(url)
48 | except Exception as e:
49 | logging.error(f"Failed to load URL {url}: {str(e)}")
50 | return None
51 |
52 | try:
53 | # Get page content
54 | soup = BeautifulSoup(driver.page_source, "html.parser")
55 |
56 | # Clean and convert content
57 | content = WebCrawler._clean_content(soup)
58 |
59 | # Check if visual analysis needed
60 | requires_visual = WebCrawler._needs_visual_analysis(soup)
61 | if requires_visual:
62 | screenshot = driver.get_screenshot_as_base64()
63 | result = {
64 | 'content': content,
65 | 'screenshot': screenshot,
66 | 'needs_visual': True
67 | }
68 | else:
69 | result = {
70 | 'content': content,
71 | 'needs_visual': False
72 | }
73 |
74 | # Cache the result
75 | if cache_manager:
76 | cache_manager.cache_crawled_content(url, result)
77 |
78 | return result
79 |
80 | except Exception as e:
81 | logging.error(f"Error processing content for {url}: {str(e)}")
82 | logging.error(f"Traceback:\n{traceback.format_exc()}")
83 | return None
84 |
85 | except Exception as e:
86 | logging.error(f"Critical error processing {url}: {str(e)}")
87 | logging.error(f"Traceback:\n{traceback.format_exc()}")
88 | return None
89 |
90 | finally:
91 | if driver:
92 | driver.quit()
93 |
94 | def _create_driver(self):
95 | options = webdriver.ChromeOptions()
96 | options.add_argument("--headless")
97 | options.add_argument("--no-sandbox")
98 | options.add_argument("--disable-dev-shm-usage")
99 | return webdriver.Chrome(options=options)
100 |
101 | def _take_screenshot(self, driver):
102 | return driver.get_screenshot_as_base64()
103 |
104 | def _setup_logging(self):
105 | self.logger = logging.getLogger(__name__)
106 | self.logger.setLevel(logging.INFO)
107 |
108 | def get_page_content(self, url):
109 | try:
110 | self.driver.set_page_load_timeout(self.timeout)
111 | self.driver.get(url)
112 | soup = BeautifulSoup(self.driver.page_source, "html.parser")
113 | return self._clean_content(soup)
114 | except Exception as e:
115 | print(f"Error crawling {url}: {str(e)}")
116 | return None
117 |
118 | @staticmethod
119 | def _clean_content(soup: BeautifulSoup) -> str:
120 | """Clean and extract main content (static method)"""
121 | # Remove unwanted elements
122 | for element in soup(["script", "style", "nav", "footer", "header"]):
123 | element.decompose()
124 |
125 | # Convert to markdown
126 | return html_to_markdown(str(soup))
127 |
128 | @staticmethod
129 | def _needs_visual_analysis(soup: BeautifulSoup) -> bool:
130 | """Check if page needs visual analysis (static method)"""
131 | # Check for tables
132 | if soup.find_all('table'):
133 | return True
134 |
135 | # Check for charts/graphs
136 | img_tags = soup.find_all('img')
137 | for img in img_tags:
138 | alt = img.get('alt', '').lower()
139 | if any(term in alt for term in ['chart', 'graph', 'diagram', 'figure']):
140 | return True
141 |
142 | return False
--------------------------------------------------------------------------------
/live-audio/frontend/public/audioWorklet.js:
--------------------------------------------------------------------------------
1 | class AudioProcessor extends AudioWorkletProcessor {
2 | constructor() {
3 | super();
4 | this.inputBuffer = [];
5 |
6 | // Remove VAD parameters - now using server-side Silero VAD only
7 | // The client-side VAD was primarily for debugging and is no longer needed
8 | }
9 |
10 | process(inputs, outputs, parameters) {
11 | const input = inputs[0];
12 | if (!input || !input[0]) return true;
13 |
14 | // Convert to mono
15 | const monoInput = new Float32Array(input[0].length);
16 | for (let i = 0; i < input[0].length; i++) {
17 | let sum = 0;
18 | for (let channel = 0; channel < input.length; channel++) {
19 | sum += input[channel][i];
20 | }
21 | monoInput[i] = sum / input.length;
22 | }
23 |
24 | // Process in chunks - removed VAD processing
25 | const CHUNK_SIZE = 1024;
26 | this.inputBuffer.push(...monoInput);
27 |
28 | while (this.inputBuffer.length >= CHUNK_SIZE) {
29 | const chunk = this.inputBuffer.slice(0, CHUNK_SIZE);
30 | this.inputBuffer = this.inputBuffer.slice(CHUNK_SIZE);
31 |
32 | // Convert to 16-bit PCM
33 | const pcmData = new Int16Array(chunk.length);
34 | for (let i = 0; i < chunk.length; i++) {
35 | const s = Math.max(-1, Math.min(1, chunk[i]));
36 | pcmData[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
37 | }
38 |
39 | // Send the data - server-side Silero VAD will handle speech detection
40 | this.port.postMessage(pcmData.buffer, [pcmData.buffer]);
41 | }
42 |
43 | return true;
44 | }
45 | }
46 |
47 | class EchoProcessor extends AudioWorkletProcessor {
48 | constructor() {
49 | super();
50 | this.audioBuffer = [];
51 | this.playbackPosition = 0;
52 | this.isPlaying = false;
53 | this.sampleRate = 16000;
54 | this.isMuted = false;
55 | this.outputBufferSize = 2048;
56 | this.outputBuffer = new Float32Array(this.outputBufferSize);
57 | this.outputBufferPosition = 0;
58 | this.hasNotifiedQueueEmpty = false; // Track if we've sent the queue empty notification
59 |
60 | this.port.onmessage = (event) => {
61 | if (event.data instanceof Float32Array) {
62 | if (!this.isMuted) {
63 | const audioData = event.data;
64 | const newBuffer = new Float32Array(audioData.length);
65 | newBuffer.set(audioData);
66 |
67 | if (!this.isPlaying) {
68 | this.audioBuffer = Array.from(newBuffer);
69 | this.playbackPosition = 0;
70 | this.outputBufferPosition = 0;
71 | this.hasNotifiedQueueEmpty = false; // Reset notification flag when starting new playback
72 | } else {
73 | this.audioBuffer.push(...Array.from(newBuffer));
74 | }
75 |
76 | this.isPlaying = true;
77 | }
78 | } else if (event.data.type === 'clear') {
79 | // Clear the buffer and stop playback
80 | this.audioBuffer = [];
81 | this.playbackPosition = 0;
82 | this.outputBufferPosition = 0;
83 | this.isPlaying = false;
84 | this.isMuted = true;
85 | this.hasNotifiedQueueEmpty = false;
86 | // Notify that the queue is empty after clearing
87 | this.port.postMessage({ type: 'queue_empty' });
88 | } else if (event.data.type === 'unmute') {
89 | this.isMuted = false;
90 | this.hasNotifiedQueueEmpty = false;
91 | } else if (event.data.type === 'mute') {
92 | this.isMuted = true;
93 | this.audioBuffer = [];
94 | this.playbackPosition = 0;
95 | this.isPlaying = false;
96 | this.hasNotifiedQueueEmpty = false;
97 | // Notify that the queue is empty after muting
98 | this.port.postMessage({ type: 'queue_empty' });
99 | }
100 | };
101 | }
102 |
103 | process(inputs, outputs, parameters) {
104 | const output = outputs[0];
105 |
106 | // If muted or not playing, output silence
107 | if (this.isMuted || !this.isPlaying || this.audioBuffer.length === 0) {
108 | for (let channel = 0; channel < output.length; channel++) {
109 | output[channel].fill(0);
110 | }
111 |
112 | // Send queue_empty notification if we haven't already
113 | if (this.isPlaying && !this.hasNotifiedQueueEmpty) {
114 | this.port.postMessage({ type: 'queue_empty' });
115 | this.hasNotifiedQueueEmpty = true;
116 | this.isPlaying = false;
117 | }
118 |
119 | return true;
120 | }
121 |
122 | const outputChannel = output[0];
123 | const bufferSize = outputChannel.length;
124 |
125 | if (this.isPlaying && this.audioBuffer.length > 0) {
126 | // Fill the output buffer
127 | for (let i = 0; i < bufferSize; i++) {
128 | if (this.playbackPosition < this.audioBuffer.length) {
129 | const sample = this.audioBuffer[this.playbackPosition];
130 | for (let channel = 0; channel < output.length; channel++) {
131 | output[channel][i] = sample;
132 | }
133 | this.playbackPosition++;
134 | } else {
135 | // End of buffer reached
136 | for (let channel = 0; channel < output.length; channel++) {
137 | output[channel][i] = 0;
138 | }
139 |
140 | // If we've played everything, reset and notify
141 | if (this.playbackPosition >= this.audioBuffer.length && !this.hasNotifiedQueueEmpty) {
142 | this.isPlaying = false;
143 | this.playbackPosition = 0;
144 | this.audioBuffer = [];
145 | this.port.postMessage({ type: 'queue_empty' });
146 | this.hasNotifiedQueueEmpty = true;
147 | }
148 | }
149 | }
150 | } else {
151 | // Output silence if we're not playing
152 | for (let channel = 0; channel < output.length; channel++) {
153 | output[channel].fill(0);
154 | }
155 | }
156 |
157 | return true;
158 | }
159 | }
160 |
161 | registerProcessor('audio-processor', AudioProcessor);
162 | registerProcessor('echo-processor', EchoProcessor);
163 |
--------------------------------------------------------------------------------
/live-audio/backend/utils/providers/llmProviders.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 |
3 | /**
4 | * Base LLM Provider class
5 | */
6 | class BaseLLMProvider {
7 | constructor(config, apiKey) {
8 | this.config = config;
9 | this.apiKey = apiKey;
10 | }
11 |
12 | async createChatCompletion(messages, options = {}) {
13 | throw new Error('createChatCompletion method must be implemented by subclass');
14 | }
15 |
16 | /**
17 | * Prepare headers for API request
18 | * @returns {Object} Headers object
19 | */
20 | getHeaders() {
21 | return {
22 | 'Authorization': `Bearer ${this.apiKey}`,
23 | 'Content-Type': 'application/json'
24 | };
25 | }
26 |
27 | /**
28 | * Prepare request payload
29 | * @param {Array} messages - Chat messages
30 | * @param {Object} options - Additional options
31 | * @returns {Object} Request payload
32 | */
33 | getRequestPayload(messages, options = {}) {
34 | return {
35 | model: this.config.model,
36 | messages: messages,
37 | stream: true,
38 | max_tokens: options.max_tokens || 4096,
39 | ...options
40 | };
41 | }
42 | }
43 |
44 | /**
45 | * OpenAI LLM Provider
46 | */
47 | class OpenAILLMProvider extends BaseLLMProvider {
48 | async createChatCompletion(messages, options = {}) {
49 | try {
50 | const payload = this.getRequestPayload(messages, options);
51 | const headers = this.getHeaders();
52 |
53 | console.log('OpenAI LLM Request:', {
54 | model: payload.model,
55 | messagesCount: messages.length,
56 | stream: payload.stream
57 | });
58 |
59 | const response = await axios.post(
60 | this.config.apiUrl,
61 | payload,
62 | {
63 | headers: headers,
64 | cancelToken: options.cancelToken,
65 | responseType: 'stream'
66 | }
67 | );
68 |
69 | return {
70 | success: true,
71 | response: response,
72 | provider: 'openai'
73 | };
74 |
75 | } catch (error) {
76 | console.error('OpenAI LLM error:', error.response?.data || error.message);
77 | return {
78 | success: false,
79 | error: error.response?.data?.error?.message || error.message,
80 | provider: 'openai'
81 | };
82 | }
83 | }
84 | }
85 |
86 | /**
87 | * OpenRouter LLM Provider (supports both GPT-4o and Gemini)
88 | */
89 | class OpenRouterLLMProvider extends BaseLLMProvider {
90 | getHeaders() {
91 | return {
92 | 'Authorization': `Bearer ${this.apiKey}`,
93 | 'Content-Type': 'application/json',
94 | 'HTTP-Referer': 'https://live-audio-chat.local',
95 | 'X-Title': 'Live Audio Chat'
96 | };
97 | }
98 |
99 | async createChatCompletion(messages, options = {}) {
100 | try {
101 | const payload = this.getRequestPayload(messages, options);
102 | const headers = this.getHeaders();
103 |
104 | console.log('OpenRouter LLM Request:', {
105 | model: payload.model,
106 | messagesCount: messages.length,
107 | stream: payload.stream
108 | });
109 |
110 | const response = await axios.post(
111 | this.config.apiUrl,
112 | payload,
113 | {
114 | headers: headers,
115 | cancelToken: options.cancelToken,
116 | responseType: 'stream'
117 | }
118 | );
119 |
120 | return {
121 | success: true,
122 | response: response,
123 | provider: 'openrouter'
124 | };
125 |
126 | } catch (error) {
127 | console.error('OpenRouter LLM error:', error.response?.data || error.message);
128 | return {
129 | success: false,
130 | error: error.response?.data?.error?.message || error.message,
131 | provider: 'openrouter'
132 | };
133 | }
134 | }
135 | }
136 |
137 | /**
138 | * ARK (Doubao) LLM Provider
139 | */
140 | class ARKLLMProvider extends BaseLLMProvider {
141 | getHeaders() {
142 | return {
143 | 'Authorization': `Bearer ${this.apiKey}`,
144 | 'Content-Type': 'application/json'
145 | };
146 | }
147 |
148 | getRequestPayload(messages, options = {}) {
149 | // ARK API format is similar to OpenAI but may have slight differences
150 | return {
151 | model: this.config.model,
152 | messages: messages,
153 | stream: true,
154 | max_tokens: options.max_tokens || 4096,
155 | temperature: options.temperature || 0.7,
156 | ...options
157 | };
158 | }
159 |
160 | async createChatCompletion(messages, options = {}) {
161 | try {
162 | const payload = this.getRequestPayload(messages, options);
163 | const headers = this.getHeaders();
164 |
165 | console.log('ARK (Doubao) LLM Request:', {
166 | model: payload.model,
167 | messagesCount: messages.length,
168 | stream: payload.stream
169 | });
170 |
171 | const response = await axios.post(
172 | this.config.apiUrl,
173 | payload,
174 | {
175 | headers: headers,
176 | cancelToken: options.cancelToken,
177 | responseType: 'stream'
178 | }
179 | );
180 |
181 | return {
182 | success: true,
183 | response: response,
184 | provider: 'ark'
185 | };
186 |
187 | } catch (error) {
188 | console.error('ARK (Doubao) LLM error:', error.response?.data || error.message);
189 | return {
190 | success: false,
191 | error: error.response?.data?.error?.message || error.message,
192 | provider: 'ark'
193 | };
194 | }
195 | }
196 | }
197 |
198 | /**
199 | * LLM Provider Factory
200 | */
201 | class LLMProviderFactory {
202 | static createProvider(providerName, config, globalConfig) {
203 | const providerConfig = config.LLM_PROVIDERS[providerName];
204 | if (!providerConfig) {
205 | throw new Error(`LLM provider ${providerName} not found in configuration`);
206 | }
207 |
208 | // Get API key from global config
209 | const apiKey = globalConfig[providerConfig.apiKey];
210 | if (!apiKey) {
211 | throw new Error(`API key ${providerConfig.apiKey} not found in configuration`);
212 | }
213 |
214 | switch (providerName) {
215 | case 'openai':
216 | return new OpenAILLMProvider(providerConfig, apiKey);
217 | case 'openrouter-gpt4o':
218 | case 'openrouter-gemini':
219 | return new OpenRouterLLMProvider(providerConfig, apiKey);
220 | case 'ark':
221 | return new ARKLLMProvider(providerConfig, apiKey);
222 | default:
223 | throw new Error(`Unsupported LLM provider: ${providerName}`);
224 | }
225 | }
226 | }
227 |
228 | module.exports = {
229 | BaseLLMProvider,
230 | OpenAILLMProvider,
231 | OpenRouterLLMProvider,
232 | ARKLLMProvider,
233 | LLMProviderFactory
234 | };
--------------------------------------------------------------------------------
/attention-hallucination-detection/frontend/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import AttentionHeatmap from '@/components/AttentionHeatmap';
3 | import VerdictDashboard from '@/components/VerdictDashboard';
4 | import TestCaseSelector from '@/components/TestCaseSelector';
5 | import VerificationSummary from '@/components/VerificationSummary';
6 | import AttentionDebugGraph from '@/components/AttentionDebugGraph';
7 |
8 | interface VerificationData {
9 | test_case: {
10 | name: string;
11 | context: string;
12 | query: string;
13 | };
14 | context_length: number;
15 | generated_text: string;
16 | verification_result: {
17 | sequence: string;
18 | tokens: string[];
19 | factuality_score: number; // 简化后的核心指标
20 | avg_system_attention: number;
21 | avg_user_attention: number;
22 | final_verdict: string;
23 | is_hallucination: boolean;
24 | analyses: Array<{
25 | token: string;
26 | token_id: number;
27 | position: number;
28 | system_attention?: number;
29 | user_attention?: number;
30 | factuality_score?: number;
31 | attention_weights: number[];
32 | }>;
33 | verdict_details: any;
34 | };
35 | attention_heatmap: {
36 | tokens: string[];
37 | attention_weights: number[][];
38 | context_boundary: number;
39 | system_prompt_boundary?: number;
40 | };
41 | }
42 |
43 | export default function Home() {
44 | const [data, setData] = useState