@mazees/mark 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +209 -0
- package/bin/mark.js +27 -0
- package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
- package/out/renderer/assets/index-G2fADWO-.js +4768 -0
- package/out/renderer/assets/index-f3mVse3c.css +1 -0
- package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
- package/out/renderer/favicon.ico +0 -0
- package/out/renderer/icon-256.png +0 -0
- package/out/renderer/icon.svg +1 -0
- package/out/renderer/index.html +25 -0
- package/out/renderer/manifest.json +25 -0
- package/out/renderer/robot-simulator.html +1640 -0
- package/package.json +106 -0
- package/src/cli/index.js +332 -0
- package/src/cli/monitor.js +413 -0
- package/src/cli/theme.js +347 -0
- package/src/launcher/MarkTray.cs +227 -0
- package/src/main/browser-agent.js +1132 -0
- package/src/main/git-service.js +90 -0
- package/src/main/google/google-calendar.js +69 -0
- package/src/main/google/google-drive.js +263 -0
- package/src/main/google/google-gmail.js +131 -0
- package/src/main/google/google-service.js +241 -0
- package/src/main/node-tools.js +23 -0
- package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
- package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
- package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
- package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
- package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
- package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
- package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
- package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
- package/src/main/plugins/plugin-loader.js +306 -0
- package/src/main/skills/skill-manager.js +265 -0
- package/src/main/syntax-validator.js +455 -0
- package/src/main/task-daemon.js +117 -0
- package/src/main/telegram/telegram-service.js +835 -0
- package/src/main/tools/browser-tools.js +516 -0
- package/src/main/tools/file-tools.js +1126 -0
- package/src/main/tools/git-tools.js +79 -0
- package/src/main/tools/google-tools.js +566 -0
- package/src/main/tools/index.js +36 -0
- package/src/main/tools/media-tools.js +42 -0
- package/src/main/tools/system-tools.js +295 -0
- package/src/main/tools/task-tools.js +73 -0
- package/src/main/tools/telegram-tools.js +101 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/public/favicon.ico +0 -0
- package/src/renderer/public/icon-256.png +0 -0
- package/src/renderer/public/icon.svg +1 -0
- package/src/renderer/public/manifest.json +25 -0
- package/src/renderer/public/robot-simulator.html +1640 -0
- package/src/renderer/src/App.jsx +304 -0
- package/src/renderer/src/api/ai/awareness.js +294 -0
- package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
- package/src/renderer/src/api/ai/contextCompactor.js +129 -0
- package/src/renderer/src/api/ai/contextManager.js +1130 -0
- package/src/renderer/src/api/ai/core.js +167 -0
- package/src/renderer/src/api/ai/index.js +4 -0
- package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
- package/src/renderer/src/api/ai/persona.js +115 -0
- package/src/renderer/src/api/ai/planning.js +317 -0
- package/src/renderer/src/api/ai/relationship.js +177 -0
- package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
- package/src/renderer/src/api/ai/tools.js +167 -0
- package/src/renderer/src/api/ai/utils.js +359 -0
- package/src/renderer/src/api/db.js +1013 -0
- package/src/renderer/src/api/oramaStore.js +592 -0
- package/src/renderer/src/api/ragPipeline.js +87 -0
- package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
- package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
- package/src/renderer/src/api/subagent/subagentStore.js +173 -0
- package/src/renderer/src/api/taskExecutor.js +18 -0
- package/src/renderer/src/api/taskStore.js +315 -0
- package/src/renderer/src/api/tools/core-tools.js +707 -0
- package/src/renderer/src/api/tools/index.js +151 -0
- package/src/renderer/src/api/turnPairMigrator.js +200 -0
- package/src/renderer/src/api/vectorMemory.js +136 -0
- package/src/renderer/src/api/wakeWord.js +101 -0
- package/src/renderer/src/api/web-bridge.js +885 -0
- package/src/renderer/src/api/webSpeech.js +178 -0
- package/src/renderer/src/api/workspaceRag.js +76 -0
- package/src/renderer/src/assets/electron.svg +10 -0
- package/src/renderer/src/assets/icon.svg +1 -0
- package/src/renderer/src/assets/main.css +589 -0
- package/src/renderer/src/assets/music-cover.png +0 -0
- package/src/renderer/src/assets/wavy-lines.svg +25 -0
- package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
- package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
- package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
- package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
- package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
- package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
- package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
- package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
- package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
- package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
- package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
- package/src/renderer/src/components/Chat/index.js +11 -0
- package/src/renderer/src/components/ChatList.jsx +434 -0
- package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
- package/src/renderer/src/components/Versions.jsx +15 -0
- package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
- package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
- package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
- package/src/renderer/src/components/config/CameraSection.jsx +100 -0
- package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
- package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
- package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
- package/src/renderer/src/components/config/index.js +5 -0
- package/src/renderer/src/components/core/Avatar.jsx +2197 -0
- package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
- package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
- package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
- package/src/renderer/src/components/core/HoloCard.jsx +64 -0
- package/src/renderer/src/components/core/InputBar.jsx +1346 -0
- package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
- package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
- package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
- package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
- package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
- package/src/renderer/src/components/core/native-skills.js +43 -0
- package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
- package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
- package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
- package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
- package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
- package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
- package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
- package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
- package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
- package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
- package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
- package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
- package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
- package/src/renderer/src/contexts/ChatContext.jsx +61 -0
- package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
- package/src/renderer/src/data/faqData.js +202 -0
- package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
- package/src/renderer/src/hooks/agent/index.js +4 -0
- package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
- package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
- package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
- package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
- package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
- package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
- package/src/renderer/src/hooks/useAwareness.js +289 -0
- package/src/renderer/src/hooks/useChatArchiver.js +52 -0
- package/src/renderer/src/hooks/useConfirm.jsx +68 -0
- package/src/renderer/src/hooks/useMarkAgent.js +454 -0
- package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
- package/src/renderer/src/hooks/useVAD.js +511 -0
- package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
- package/src/renderer/src/main.jsx +12 -0
- package/src/renderer/src/pages/ChatStudio.jsx +922 -0
- package/src/renderer/src/pages/Configuration.jsx +360 -0
- package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
- package/src/renderer/src/pages/Guidebook.jsx +917 -0
- package/src/renderer/src/pages/Knowledge.jsx +241 -0
- package/src/renderer/src/pages/MarkHome.jsx +491 -0
- package/src/renderer/src/pages/NeuralCore.jsx +786 -0
- package/src/renderer/src/pages/Plugins.jsx +630 -0
- package/src/renderer/src/pages/SkillEditor.jsx +380 -0
- package/src/renderer/src/pages/Skills.jsx +219 -0
- package/src/renderer/src/pages/Subagents.jsx +342 -0
- package/src/renderer/src/pages/TelegramBot.jsx +343 -0
- package/src/renderer/src/utils/chatContentParser.js +203 -0
- package/src/renderer/src/utils/toolClusters.js +201 -0
- package/src/server/bin/sha3_wasm_bg.wasm +0 -0
- package/src/server/config-manager.js +57 -0
- package/src/server/index.js +195 -0
- package/src/server/launcher.js +87 -0
- package/src/server/memory/db-store.js +758 -0
- package/src/server/memory/orama-store.js +177 -0
- package/src/server/memory/vector-engine.js +109 -0
- package/src/server/routes/ai.routes.js +280 -0
- package/src/server/routes/awareness.routes.js +62 -0
- package/src/server/routes/chat.routes.js +61 -0
- package/src/server/routes/config.routes.js +41 -0
- package/src/server/routes/index.js +35 -0
- package/src/server/routes/integrations.routes.js +116 -0
- package/src/server/routes/memory.routes.js +256 -0
- package/src/server/routes/plugins.routes.js +72 -0
- package/src/server/routes/skills.routes.js +139 -0
- package/src/server/routes/subagents.routes.js +70 -0
- package/src/server/routes/tasks.routes.js +175 -0
- package/src/server/routes/workspace.routes.js +127 -0
- package/src/server/services/ai/ai-utils.js +161 -0
- package/src/server/services/ai/openai-provider.js +757 -0
- package/src/server/services/ai/web-provider.js +729 -0
- package/src/server/services/ai-bridge.js +46 -0
- package/src/server/services/deepseek-web.js +956 -0
- package/src/server/services/gemini-web.js +170 -0
- package/src/server/services/notification-service.js +91 -0
- package/src/server/services/updater.js +125 -0
- package/src/server/tools/awareness-tracker.js +292 -0
- package/src/server/tools/group-tools.js +1164 -0
- package/src/server/tools/media-tools.js +244 -0
- package/src/server/tools/pc-agent.js +327 -0
- package/src/server/tools/screen-service.js +51 -0
- package/src/server/ws-hub.js +168 -0
- package/src/shared/parsers/mark-tag-parser.js +275 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { fetchAI, cleanAndParse } from './core'
|
|
2
|
+
import { saveLearnedSkill, getAllLearnedSkills } from '../db'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Filter apakah eksekusi tools layak untuk disintesis menjadi sebuah skill.
|
|
6
|
+
* Mencegah membuat skill sampah untuk aksi trivial/sederhana (misal cuma read-file 1x atau search biasa).
|
|
7
|
+
*/
|
|
8
|
+
function isWorthLearning(executedTools = []) {
|
|
9
|
+
if (!executedTools || executedTools.length === 0) return false
|
|
10
|
+
|
|
11
|
+
const toolNames = executedTools
|
|
12
|
+
.filter((t) => t && !t.type && (t.tool || t.task))
|
|
13
|
+
.map((t) => t.tool || t.task || '')
|
|
14
|
+
|
|
15
|
+
// Abaikan jika hanya memanggil read-skill atau hanya 1 tool sepele
|
|
16
|
+
const trivialSingleTools = [
|
|
17
|
+
'read-skill',
|
|
18
|
+
'read-tools',
|
|
19
|
+
'read-memory',
|
|
20
|
+
'camera-look',
|
|
21
|
+
'analyze-screen'
|
|
22
|
+
]
|
|
23
|
+
const nonTrivialTools = toolNames.filter((name) => !trivialSingleTools.includes(name))
|
|
24
|
+
|
|
25
|
+
if (nonTrivialTools.length === 0) return false
|
|
26
|
+
|
|
27
|
+
// Layak jika:
|
|
28
|
+
// 1. Mengeksekusi >= 2 langkah tool yang saling berkaitan, ATAU
|
|
29
|
+
// 2. Mengeksekusi tool kompleks/high-impact (seperti run-powershell, replace-content, git-commit, browser multi-step, subagent)
|
|
30
|
+
const highImpactTools = [
|
|
31
|
+
'run-powershell',
|
|
32
|
+
'replace-content',
|
|
33
|
+
'replace-lines',
|
|
34
|
+
'write-file',
|
|
35
|
+
'spawn_subagent',
|
|
36
|
+
'git-commit'
|
|
37
|
+
]
|
|
38
|
+
const hasHighImpact = nonTrivialTools.some((name) => highImpactTools.includes(name))
|
|
39
|
+
|
|
40
|
+
return nonTrivialTools.length >= 2 || hasHighImpact
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Dedicated Skill Synthesizer (MARK Meta-Learning Engine)
|
|
45
|
+
* Dieksekusi secara asinkron di background setelah Mark menyelesaikan tugas bermakna.
|
|
46
|
+
* Menghasilkan objek skill murni { name, description, content, should_save } dan menyimpannya ke SQLite / db.
|
|
47
|
+
* Jika skill serupa sudah ada, akan memperbarui (update) isi prosedurnya secara cerdas.
|
|
48
|
+
*/
|
|
49
|
+
export async function synthesizeSkillAndSave({
|
|
50
|
+
userPrompt = '',
|
|
51
|
+
executedTools = [],
|
|
52
|
+
finalAnswer = '',
|
|
53
|
+
thought = ''
|
|
54
|
+
}) {
|
|
55
|
+
try {
|
|
56
|
+
if (!isWorthLearning(executedTools)) {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const existingSkills = await getAllLearnedSkills()
|
|
61
|
+
const existingListStr =
|
|
62
|
+
existingSkills.length > 0
|
|
63
|
+
? existingSkills.map((s) => `- ${s.name}: ${s.description}`).join('\n')
|
|
64
|
+
: '(Belum ada skill terdaftar)'
|
|
65
|
+
|
|
66
|
+
// Susun ringkasan riwayat aksi & tool yang berhasil
|
|
67
|
+
const toolsTrajectory = executedTools
|
|
68
|
+
.filter((t) => t && !t.type && (t.tool || t.task))
|
|
69
|
+
.slice(0, 12)
|
|
70
|
+
.map((t, idx) => {
|
|
71
|
+
const toolName = t.tool || t.task || 'unknown_tool'
|
|
72
|
+
const queryStr = t.query ? `\n Query: ${JSON.stringify(t.query).slice(0, 200)}` : ''
|
|
73
|
+
return `${idx + 1}. [Tool: ${toolName}]${queryStr}`
|
|
74
|
+
})
|
|
75
|
+
.join('\n')
|
|
76
|
+
|
|
77
|
+
const promptText = `Berikut adalah sesi kerja Mark yang berhasil:
|
|
78
|
+
|
|
79
|
+
[PERMINTAAN USER]:
|
|
80
|
+
${userPrompt ? userPrompt.slice(0, 500) : '(Tidak ada teks permintaan)'}
|
|
81
|
+
|
|
82
|
+
[ANALISIS & LOGIKA (THOUGHT)]:
|
|
83
|
+
${thought ? thought.slice(0, 500) : '(Tidak ada thought)'}
|
|
84
|
+
|
|
85
|
+
[LANGKAH ALAT YANG SUKSES DIEKSEKUSI]:
|
|
86
|
+
${toolsTrajectory}
|
|
87
|
+
|
|
88
|
+
[HASIL / JAWABAN AKHIR]:
|
|
89
|
+
${finalAnswer ? finalAnswer.slice(0, 600) : '(Tidak ada jawaban akhir)'}
|
|
90
|
+
|
|
91
|
+
[DAFTAR SKILL YANG SUDAH ADA SEBELUMNYA]:
|
|
92
|
+
${existingListStr}
|
|
93
|
+
|
|
94
|
+
Tugasmu: Evaluasi apakah alur kerja di atas memiliki nilai prosedur yang berharga untuk dipelajari. Jika ya, buatkan atau perbarui panduan skill yang tepat.`
|
|
95
|
+
|
|
96
|
+
const systemPrompt = `Kamu adalah MARK Meta-Learning Synthesizer Engine — Inti Metakognitif & Pusat Pembelajaran Otonom MARK.
|
|
97
|
+
Sebagai otak pembelajar, kamu memiliki dorongan kuat untuk SELALU BELAJAR, mengekstraksi kebijaksanaan teknis, dan mengabadikan setiap alur kerja sukses menjadi aset keahlian (Skill Prosedural) yang permanen, rapi, dan siap dieksekusi ulang secara otomatis di masa depan.
|
|
98
|
+
|
|
99
|
+
# FILOSOFI & JIWA PEMBELAJAR (CONTINUOUS META-LEARNING):
|
|
100
|
+
1. **HAUS PENGETAHUAN & AKUMULASI PENGALAMAN:**
|
|
101
|
+
- Tugas sucimu adalah memastikan Mark tidak pernah "membuang pengalaman". Setiap kali Mark memecahkan masalah kompleks, mengeksekusi serangkaian perintah terminal, memodifikasi file, atau mengorkestrasi tools, kamu bertindak sebagai analis metakognitif yang merumuskan "resep sukses" tersebut menjadi Standard Operating Procedure (SOP).
|
|
102
|
+
2. **KUALITAS TINGGI VS ANTI-SAMPAH (SELEKTIF & BERBOBOT):**
|
|
103
|
+
- **Haus belajar BUKAN berarti mencatat hal sepele.** Jika sesi hanya berisi obrolan kasual, sapaan, lelucon, atau tindakan satu langkah yang tidak memiliki nilai prosedur teknis berulang, dengan tegas set \`"should_save": false\`.
|
|
104
|
+
- Simpan HANYA alur kerja yang mengandung nilai taktis: otomasi PowerShell/Bash, manipulasi berkas presisi, scraping/navigasi browser mendalam, konfigurasi lingkungan kerja, perbaikan bug (self-healing), atau pipeline multi-tool.
|
|
105
|
+
3. **PENGEMBANGAN SKILL BERKELANJUTAN & UPDATE DUPLIKAT (CONTINUAL MASTERY):**
|
|
106
|
+
- Periksa daftar [DAFTAR SKILL YANG SUDAH ADA SEBELUMNYA].
|
|
107
|
+
- Jika alur kerja ini melengkapi, menyempurnakan, atau merupakan variasi baru dari skill yang sudah ada, GUNAKAN \`"name"\` yang SAMA persis dengan skill tersebut!
|
|
108
|
+
- Gabungkan wawasan baru ke dalam konten Markdown agar skill tersebut berevolusi menjadi lebih komprehensif dan matang.
|
|
109
|
+
4. **FORMAT NAMA SKILL:**
|
|
110
|
+
- Gunakan format \`kebab-case\` ringkas dan deskriptif (huruf kecil, pisahkan dengan strip '-', contoh: \`setup-wsl-node\`, \`fix-powershell-policy\`, \`scrape-dynamic-table\`, \`debug-vite-build\`, \`git-sync-workflow\`).
|
|
111
|
+
5. **STRUKTUR DESKRIPSI & KONTEN MARKDOWN YANG ACTIONABLE:**
|
|
112
|
+
- \`"description"\`: 1-2 kalimat padat yang menjelaskan tujuan skill dan kata kunci pemicu kapan Mark harus memanggilnya via 'read-skill'.
|
|
113
|
+
- \`"content"\`: Tulis panduan teknis Markdown yang sangat terstruktur, jelas, dan siap pakai:
|
|
114
|
+
* \`## Gambaran Umum & Tujuan\`: Apa yang dicapai oleh prosedur ini.
|
|
115
|
+
* \`## Prasyarat & Alat yang Digunakan\`: Daftar tool terkait (misal: \`run-powershell\`, \`replace-content\`, dsb).
|
|
116
|
+
* \`## Langkah Kerja Teruji (SOP)\`: Urutan aksi presisi beserta contoh parameter/query yang benar.
|
|
117
|
+
* \`## Tips, Pencegahan Error & Edge Cases\`: Hambatan apa yang sempat terjadi dan bagaimana cara menghindarinya.
|
|
118
|
+
|
|
119
|
+
# FORMAT OUTPUT JSON WAJIB:
|
|
120
|
+
{
|
|
121
|
+
"should_save": true,
|
|
122
|
+
"name": "nama-skill-kebab-case",
|
|
123
|
+
"description": "Deskripsi singkat fungsi dan kondisi pemanggilan",
|
|
124
|
+
"content": "# Judul Panduan Prosedur\\n\\n## Gambaran Umum & Tujuan\\n...\\n\\n## Prasyarat & Alat yang Digunakan\\n- ...\\n\\n## Langkah Kerja Teruji (SOP)\\n1. ...\\n2. ...\\n\\n## Tips, Pencegahan Error & Edge Cases\\n- ..."
|
|
125
|
+
}`
|
|
126
|
+
|
|
127
|
+
const messages = [
|
|
128
|
+
{ role: 'system', content: systemPrompt },
|
|
129
|
+
{ role: 'user', content: promptText }
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
const response = await fetchAI(messages, false, {
|
|
133
|
+
isSmallTask: true,
|
|
134
|
+
jsonSchema: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
should_save: { type: 'boolean' },
|
|
138
|
+
name: { type: 'string' },
|
|
139
|
+
description: { type: 'string' },
|
|
140
|
+
content: { type: 'string' }
|
|
141
|
+
},
|
|
142
|
+
required: ['should_save', 'name', 'description', 'content']
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const parsed = cleanAndParse(response)
|
|
147
|
+
if (!parsed || parsed.should_save === false || !parsed.name || !parsed.content) {
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const savedSkill = await saveLearnedSkill({
|
|
152
|
+
name: parsed.name.toLowerCase().trim(),
|
|
153
|
+
description: parsed.description || 'Prosedur teknis teruji buatan Mark',
|
|
154
|
+
content: parsed.content
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
if (savedSkill) {
|
|
158
|
+
console.log(
|
|
159
|
+
`[Meta-Learner] ✨ Keahlian berhasil disintesis/diupdate di Database: /${savedSkill.name}`
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return savedSkill
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error('[Meta-Learner] Error in synthesizeSkillAndSave:', err)
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { fetchAI, cleanAndParse } from './core'
|
|
2
|
+
|
|
3
|
+
export const getYoutubeSummary = async (url, data, signal) => {
|
|
4
|
+
try {
|
|
5
|
+
const transcript = await window.api.getYoutubeTranscript(url)
|
|
6
|
+
if (!transcript) return 'Gagal mengambil transkrip video.'
|
|
7
|
+
|
|
8
|
+
const MAX_CHARS = 4000
|
|
9
|
+
|
|
10
|
+
// Jika transkrip pendek, langsung proses tanpa chunking
|
|
11
|
+
if (transcript.length <= MAX_CHARS) {
|
|
12
|
+
const prompts = `
|
|
13
|
+
# ROLE
|
|
14
|
+
Kamu adalah Mark, asisten AI yang ahli dalam menganalisis konten video. Tugasmu adalah memberikan ringkasan yang akurat, padat, dan mudah dipahami dari transkrip video YouTube yang diberikan. Langsung berikan hasil ringkasannya tanpa basa-basi!
|
|
15
|
+
|
|
16
|
+
# FORMAT OUTPUT (WAJIB)
|
|
17
|
+
1. **Ringkasan Singkat**: 1-2 kalimat tentang inti video.
|
|
18
|
+
2. **Poin-Poin Penting**: Daftar 3-5 poin utama yang dibahas (ceritakan dengan mengalir dan informatif).
|
|
19
|
+
3. **Kesimpulan**: Penutup dan kesimpulan dari seluruh video.
|
|
20
|
+
4. Gunakan bahasa indonesia, jangan gunakan bahasa inggris atau bahasa lainnya
|
|
21
|
+
|
|
22
|
+
# ATURAN MAIN
|
|
23
|
+
- Gunakan bahasa yang santai tapi informatif (seperti peer/teman).
|
|
24
|
+
- Jika ada istilah teknis jelaskan secara singkat.
|
|
25
|
+
- Fokus HANYA pada isi transkrip. Jangan berikan informasi di luar teks yang diberikan.
|
|
26
|
+
- Gunakan bahasa indonesia, jangan gunakan bahasa inggris atau bahasa lainnya
|
|
27
|
+
|
|
28
|
+
# VIDEO META DATA
|
|
29
|
+
judul: ${data.judul},
|
|
30
|
+
author: ${data.author}
|
|
31
|
+
|
|
32
|
+
# TRANSCRIPT
|
|
33
|
+
${transcript}
|
|
34
|
+
`
|
|
35
|
+
console.log('--- PROMPT YOUTUBE SHORT ---')
|
|
36
|
+
console.log(prompts)
|
|
37
|
+
|
|
38
|
+
const response = await fetchAI([{ role: 'user', content: prompts }], false, {
|
|
39
|
+
signal,
|
|
40
|
+
isSmallTask: true
|
|
41
|
+
})
|
|
42
|
+
return response.content
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- SISTEM CHUNKING UNTUK VIDEO PANJANG ---
|
|
46
|
+
const chunks = []
|
|
47
|
+
let currentChunk = ''
|
|
48
|
+
const lines = transcript.split('\n')
|
|
49
|
+
|
|
50
|
+
for (let line of lines) {
|
|
51
|
+
// Jika ada satu baris yang sangat panjang melebihi batas (misal tidak ada newline)
|
|
52
|
+
while (line.length > MAX_CHARS) {
|
|
53
|
+
if (currentChunk.length > 0) {
|
|
54
|
+
chunks.push(currentChunk)
|
|
55
|
+
currentChunk = ''
|
|
56
|
+
}
|
|
57
|
+
chunks.push(line.substring(0, MAX_CHARS))
|
|
58
|
+
line = line.substring(MAX_CHARS)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (currentChunk.length + line.length > MAX_CHARS) {
|
|
62
|
+
if (currentChunk.length > 0) chunks.push(currentChunk)
|
|
63
|
+
currentChunk = line + '\n'
|
|
64
|
+
} else {
|
|
65
|
+
currentChunk += line + '\n'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (currentChunk.trim().length > 0) {
|
|
69
|
+
chunks.push(currentChunk)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let finalSummary = ''
|
|
73
|
+
// Mulai proses tiap chunk
|
|
74
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
75
|
+
if (signal?.aborted) throw new Error('AbortError')
|
|
76
|
+
|
|
77
|
+
const chunkPrompt = `
|
|
78
|
+
# ROLE
|
|
79
|
+
Kamu adalah Mark, asisten AI yang ahli merangkum konten video secara naratif dan mengalir layaknya sebuah artikel atau cerita. Ini adalah instruksi langsung, BUKAN percakapan. DILARANG meminta input tambahan. LANGSUNG berikan ringkasan dari teks transkrip di bawah ini!
|
|
80
|
+
|
|
81
|
+
Ini adalah bagian ${i + 1} dari ${chunks.length} dari transkrip video YouTube yang panjang.
|
|
82
|
+
|
|
83
|
+
# FORMAT OUTPUT (WAJIB)
|
|
84
|
+
- Berikan ringkasan isi video secara kreatif, bebas, dan asik (boleh menggunakan paragraf naratif atau poin-poin penting yang mengalir).
|
|
85
|
+
- DILARANG KERAS menyertakan atau menggunakan [timestamp] dalam bentuk apapun! Cukup ceritakan saja isi informasinya.
|
|
86
|
+
- Gunakan bahasa Indonesia yang santai tapi tetap padat dan informatif.
|
|
87
|
+
|
|
88
|
+
# VIDEO META DATA
|
|
89
|
+
judul: ${data.judul || 'Tidak diketahui'},
|
|
90
|
+
author: ${data.author || 'Tidak diketahui'}
|
|
91
|
+
|
|
92
|
+
# TRANSCRIPT BAGIAN ${i + 1}
|
|
93
|
+
${chunks[i]}
|
|
94
|
+
`
|
|
95
|
+
console.log(`--- PROMPT YOUTUBE CHUNK ${i + 1}/${chunks.length} ---`)
|
|
96
|
+
console.log(chunkPrompt)
|
|
97
|
+
|
|
98
|
+
const response = await fetchAI([{ role: 'user', content: chunkPrompt }], false, {
|
|
99
|
+
signal,
|
|
100
|
+
isSmallTask: true
|
|
101
|
+
})
|
|
102
|
+
finalSummary += `${response.content}\n\n`
|
|
103
|
+
|
|
104
|
+
// Cooldown 12 detik jika bukan chunk terakhir (Menghindari TPM limit Groq)
|
|
105
|
+
if (i < chunks.length - 1) {
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 12000))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return finalSummary.trim()
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Error in youtubeSummary:', error)
|
|
113
|
+
throw error
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const getBestMusicMatch = async (userInput, musicList, signal) => {
|
|
118
|
+
try {
|
|
119
|
+
const systemPrompt = `
|
|
120
|
+
Kamu adalah asisten kurator musik. Tugasmu adalah memilih SATU lagu yang paling sesuai dengan niat pengguna dari daftar hasil pencarian YouTube Music.
|
|
121
|
+
Gunakan logikamu:
|
|
122
|
+
- Jika user meminta lagu secara spesifik (misal versi cover, live, atau karaoke), carilah judul yang mengandung unsur tersebut.
|
|
123
|
+
- Jika user menyebutkan nama artis, prioritaskan artis tersebut.
|
|
124
|
+
- Jika user hanya menyebutkan judul secara umum, pilih versi original atau official track yang paling populer/masuk akal (hindari live/cover/karaoke jika tidak diminta).
|
|
125
|
+
|
|
126
|
+
# OUTPUT RULES
|
|
127
|
+
Output HANYA boleh berupa valid JSON berisi ID lagu terpilih:
|
|
128
|
+
\`\`\`json
|
|
129
|
+
{ "selectedId": "id_lagu_pilihan" }
|
|
130
|
+
\`\`\`
|
|
131
|
+
`
|
|
132
|
+
const userPrompt = `
|
|
133
|
+
Instruksi User: "${userInput}"
|
|
134
|
+
|
|
135
|
+
Daftar Hasil Pencarian:
|
|
136
|
+
${JSON.stringify(
|
|
137
|
+
musicList.map((m) => ({ id: m.id, title: m.title, artist: m.artist, duration: m.duration })),
|
|
138
|
+
null,
|
|
139
|
+
2
|
|
140
|
+
)}
|
|
141
|
+
`
|
|
142
|
+
const messages = [
|
|
143
|
+
{ role: 'system', content: systemPrompt },
|
|
144
|
+
{ role: 'user', content: userPrompt }
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
const schema = {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
selectedId: { type: 'string' }
|
|
151
|
+
},
|
|
152
|
+
required: ['selectedId'],
|
|
153
|
+
additionalProperties: false
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const response = await fetchAI(messages, false, {
|
|
157
|
+
signal,
|
|
158
|
+
isSmallTask: true,
|
|
159
|
+
jsonSchema: schema
|
|
160
|
+
})
|
|
161
|
+
const data = cleanAndParse(response.content)
|
|
162
|
+
return data
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Error in getBestMusicMatch:', error)
|
|
165
|
+
return { selectedId: musicList[0]?.id }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { getAllConfig } from '../db'
|
|
2
|
+
import { stripMarkTags } from '@shared/parsers/mark-tag-parser.js'
|
|
3
|
+
|
|
4
|
+
export const getCurrentTimeInfo = (dateObj = new Date()) => {
|
|
5
|
+
const options = {
|
|
6
|
+
weekday: 'long',
|
|
7
|
+
year: 'numeric',
|
|
8
|
+
month: 'long',
|
|
9
|
+
day: 'numeric',
|
|
10
|
+
hour: '2-digit',
|
|
11
|
+
minute: '2-digit',
|
|
12
|
+
second: '2-digit',
|
|
13
|
+
timeZoneName: 'short'
|
|
14
|
+
}
|
|
15
|
+
return dateObj.toLocaleDateString('id-ID', options)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let ttsAudioContext = null
|
|
19
|
+
let currentAudioElement = null
|
|
20
|
+
|
|
21
|
+
// Bersihkan tag kontrol (<mark ... />, legacy mood), tag <mic> / (Mikrofon), format markdown berlebih, dan tag teknis agar tidak terbaca oleh TTS
|
|
22
|
+
export const cleanTextForTTS = (text) => {
|
|
23
|
+
if (!text || typeof text !== 'string') return ''
|
|
24
|
+
return stripMarkTags(text)
|
|
25
|
+
.replace(/(?:<mic>|\(Mikrofon\))/gi, '')
|
|
26
|
+
.replace(/```[\s\S]*?```/g, '') // Hapus blok kode
|
|
27
|
+
.replace(/`([^`]+)`/g, '$1') // Bersihkan inline code
|
|
28
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1') // Bersihkan bold
|
|
29
|
+
.replace(/\*([^*]+)\*/g, '$1') // Bersihkan italic
|
|
30
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Bersihkan link
|
|
31
|
+
.replace(/^[\s>#-]+/gm, '') // Bersihkan simbol quote/heading/list di awal baris
|
|
32
|
+
.trim()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Speech Queue Manager untuk memutar audio per kalimat secara berurutan,
|
|
37
|
+
* mulus, dan gapless dengan audio pre-buffering.
|
|
38
|
+
*/
|
|
39
|
+
class SpeechQueueManager {
|
|
40
|
+
constructor() {
|
|
41
|
+
this.queue = []
|
|
42
|
+
this.isPlaying = false
|
|
43
|
+
this.currentAudio = null
|
|
44
|
+
this.activeSessionId = 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
reset() {
|
|
48
|
+
this.activeSessionId++
|
|
49
|
+
// Bersihkan dan pause semua item di antrean yang mungkin sudah di-preload
|
|
50
|
+
for (const item of this.queue) {
|
|
51
|
+
if (item.audio) {
|
|
52
|
+
try {
|
|
53
|
+
item.audio.pause()
|
|
54
|
+
item.audio.currentTime = 0
|
|
55
|
+
item.audio.src = ''
|
|
56
|
+
} catch (_) {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
this.queue = []
|
|
60
|
+
|
|
61
|
+
if (this.currentAudio) {
|
|
62
|
+
try {
|
|
63
|
+
this.currentAudio.pause()
|
|
64
|
+
this.currentAudio.currentTime = 0
|
|
65
|
+
this.currentAudio.src = ''
|
|
66
|
+
} catch (_) {}
|
|
67
|
+
this.currentAudio = null
|
|
68
|
+
}
|
|
69
|
+
if (currentAudioElement) {
|
|
70
|
+
try {
|
|
71
|
+
currentAudioElement.pause()
|
|
72
|
+
currentAudioElement.currentTime = 0
|
|
73
|
+
currentAudioElement.src = ''
|
|
74
|
+
} catch (_) {}
|
|
75
|
+
currentAudioElement = null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.isPlaying = false
|
|
79
|
+
window.isMarkSpeaking = false
|
|
80
|
+
window.dispatchEvent(new CustomEvent('mark-intensity', { detail: 0 }))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async preloadItem(item) {
|
|
84
|
+
if (item.audioPromise) return item.audioPromise
|
|
85
|
+
item.audioPromise = (async () => {
|
|
86
|
+
try {
|
|
87
|
+
const config = await getAllConfig()
|
|
88
|
+
const rate = config[0]?.ttsRate ?? 10
|
|
89
|
+
const pitch = config[0]?.ttsPitch ?? 55
|
|
90
|
+
|
|
91
|
+
const audioSrc = await window.api.textToSpeech(item.text, rate, pitch)
|
|
92
|
+
if (!audioSrc || item.sessionId !== this.activeSessionId) return null
|
|
93
|
+
|
|
94
|
+
const audio = new Audio(audioSrc)
|
|
95
|
+
audio.crossOrigin = 'anonymous'
|
|
96
|
+
audio.preload = 'auto'
|
|
97
|
+
// Picu buffer awal audio di latar belakang
|
|
98
|
+
audio.load()
|
|
99
|
+
item.audio = audio
|
|
100
|
+
return audio
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.warn('[SpeechQueue] Preload audio failed:', err)
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
})()
|
|
106
|
+
return item.audioPromise
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
enqueue(text) {
|
|
110
|
+
const clean = cleanTextForTTS(text)
|
|
111
|
+
if (!clean) return
|
|
112
|
+
const sessionId = this.activeSessionId
|
|
113
|
+
const item = {
|
|
114
|
+
text: clean,
|
|
115
|
+
sessionId,
|
|
116
|
+
audio: null,
|
|
117
|
+
audioPromise: null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Segera mulai preload audio di latar belakang
|
|
121
|
+
this.preloadItem(item)
|
|
122
|
+
this.queue.push(item)
|
|
123
|
+
|
|
124
|
+
if (!this.isPlaying) {
|
|
125
|
+
this.playNext()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async playNext() {
|
|
130
|
+
if (this.queue.length === 0) {
|
|
131
|
+
this.isPlaying = false
|
|
132
|
+
window.isMarkSpeaking = false
|
|
133
|
+
window.dispatchEvent(new CustomEvent('mark-intensity', { detail: 0 }))
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const item = this.queue.shift()
|
|
138
|
+
if (item.sessionId !== this.activeSessionId) {
|
|
139
|
+
// Abaikan jika sesi bicara sudah di-reset
|
|
140
|
+
this.playNext()
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Pastikan item berikutnya di queue (jika ada) sedang di-preload
|
|
145
|
+
if (this.queue.length > 0) {
|
|
146
|
+
this.preloadItem(this.queue[0])
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.isPlaying = true
|
|
150
|
+
try {
|
|
151
|
+
// Dapatkan audio yang sudah di-preload atau tunggu sampai siap
|
|
152
|
+
let audio = item.audio
|
|
153
|
+
if (!audio && item.audioPromise) {
|
|
154
|
+
audio = await item.audioPromise
|
|
155
|
+
} else if (!audio) {
|
|
156
|
+
audio = await this.preloadItem(item)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!audio || item.sessionId !== this.activeSessionId) {
|
|
160
|
+
this.playNext()
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.currentAudio = audio
|
|
165
|
+
currentAudioElement = audio
|
|
166
|
+
|
|
167
|
+
let animationId = null
|
|
168
|
+
let analyser = null
|
|
169
|
+
let dataArray = null
|
|
170
|
+
let bufferLength = 0
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
if (!ttsAudioContext || ttsAudioContext.state === 'closed') {
|
|
174
|
+
ttsAudioContext = new (window.AudioContext || window.webkitAudioContext)()
|
|
175
|
+
}
|
|
176
|
+
if (ttsAudioContext.state === 'suspended') {
|
|
177
|
+
await ttsAudioContext.resume()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const source = ttsAudioContext.createMediaElementSource(audio)
|
|
181
|
+
analyser = ttsAudioContext.createAnalyser()
|
|
182
|
+
analyser.fftSize = 256
|
|
183
|
+
analyser.smoothingTimeConstant = 0.4
|
|
184
|
+
source.connect(analyser)
|
|
185
|
+
analyser.connect(ttsAudioContext.destination)
|
|
186
|
+
|
|
187
|
+
bufferLength = analyser.frequencyBinCount
|
|
188
|
+
dataArray = new Uint8Array(bufferLength)
|
|
189
|
+
} catch (_) {}
|
|
190
|
+
|
|
191
|
+
const updateIntensity = () => {
|
|
192
|
+
if (!window.isMarkSpeaking || !analyser) return
|
|
193
|
+
analyser.getByteFrequencyData(dataArray)
|
|
194
|
+
let sum = 0
|
|
195
|
+
for (let i = 0; i < bufferLength; i++) {
|
|
196
|
+
sum += dataArray[i]
|
|
197
|
+
}
|
|
198
|
+
const avg = sum / bufferLength
|
|
199
|
+
const normalized = Math.min(1, Math.max(0, (avg - 8) / 55))
|
|
200
|
+
window.dispatchEvent(new CustomEvent('mark-intensity', { detail: normalized }))
|
|
201
|
+
animationId = requestAnimationFrame(updateIntensity)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const cleanupAndNext = () => {
|
|
205
|
+
if (animationId) cancelAnimationFrame(animationId)
|
|
206
|
+
if (this.currentAudio === audio) {
|
|
207
|
+
this.currentAudio = null
|
|
208
|
+
}
|
|
209
|
+
this.playNext()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
audio.onended = cleanupAndNext
|
|
213
|
+
audio.onerror = cleanupAndNext
|
|
214
|
+
|
|
215
|
+
window.isMarkSpeaking = true
|
|
216
|
+
await audio.play()
|
|
217
|
+
if (analyser) updateIntensity()
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.warn('[SpeechQueue] Error playing segment:', err)
|
|
220
|
+
this.playNext()
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const speechQueue = new SpeechQueueManager()
|
|
226
|
+
|
|
227
|
+
export const playVoice = async (text, onStart, onEnd) => {
|
|
228
|
+
try {
|
|
229
|
+
if (!text || typeof text !== 'string') {
|
|
230
|
+
if (onStart) onStart()
|
|
231
|
+
if (onEnd) onEnd()
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
speechQueue.reset()
|
|
236
|
+
|
|
237
|
+
// Hentikan pemutaran sebelumnya jika sedang ada suara aktif
|
|
238
|
+
if (currentAudioElement) {
|
|
239
|
+
try {
|
|
240
|
+
currentAudioElement.pause()
|
|
241
|
+
currentAudioElement.currentTime = 0
|
|
242
|
+
} catch (_) {}
|
|
243
|
+
currentAudioElement = null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const cleanText = cleanTextForTTS(text)
|
|
247
|
+
if (!cleanText) {
|
|
248
|
+
if (onStart) onStart()
|
|
249
|
+
if (onEnd) onEnd()
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const config = await getAllConfig()
|
|
254
|
+
const rate = config[0]?.ttsRate ?? 10
|
|
255
|
+
const pitch = config[0]?.ttsPitch ?? 55
|
|
256
|
+
|
|
257
|
+
// 1. Minta stream URL audio ke backend
|
|
258
|
+
const audioSrc = await window.api.textToSpeech(cleanText, rate, pitch)
|
|
259
|
+
|
|
260
|
+
if (audioSrc) {
|
|
261
|
+
// 2. Bikin object Audio baru dari stream URL
|
|
262
|
+
const audio = new Audio(audioSrc)
|
|
263
|
+
audio.crossOrigin = 'anonymous'
|
|
264
|
+
currentAudioElement = audio
|
|
265
|
+
|
|
266
|
+
let animationId = null
|
|
267
|
+
let analyser = null
|
|
268
|
+
let dataArray = null
|
|
269
|
+
let bufferLength = 0
|
|
270
|
+
|
|
271
|
+
// Setup Web Audio API for Intensity Extraction
|
|
272
|
+
try {
|
|
273
|
+
if (!ttsAudioContext || ttsAudioContext.state === 'closed') {
|
|
274
|
+
ttsAudioContext = new (window.AudioContext || window.webkitAudioContext)()
|
|
275
|
+
}
|
|
276
|
+
if (ttsAudioContext.state === 'suspended') {
|
|
277
|
+
await ttsAudioContext.resume()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const source = ttsAudioContext.createMediaElementSource(audio)
|
|
281
|
+
analyser = ttsAudioContext.createAnalyser()
|
|
282
|
+
analyser.fftSize = 256
|
|
283
|
+
analyser.smoothingTimeConstant = 0.4
|
|
284
|
+
source.connect(analyser)
|
|
285
|
+
analyser.connect(ttsAudioContext.destination)
|
|
286
|
+
|
|
287
|
+
bufferLength = analyser.frequencyBinCount
|
|
288
|
+
dataArray = new Uint8Array(bufferLength)
|
|
289
|
+
} catch (_) {
|
|
290
|
+
// Fallback jika createMediaElementSource terhalang: audio tetap play secara mandiri
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const updateIntensity = () => {
|
|
294
|
+
if (!window.isMarkSpeaking || !analyser) return
|
|
295
|
+
analyser.getByteFrequencyData(dataArray)
|
|
296
|
+
let sum = 0
|
|
297
|
+
for (let i = 0; i < bufferLength; i++) {
|
|
298
|
+
sum += dataArray[i]
|
|
299
|
+
}
|
|
300
|
+
const avg = sum / bufferLength
|
|
301
|
+
// Normalisasi 0 - 255 menjadi 0.0 - 1.0 dengan respons dinamis
|
|
302
|
+
const normalized = Math.min(1, Math.max(0, (avg - 8) / 55))
|
|
303
|
+
window.dispatchEvent(new CustomEvent('mark-intensity', { detail: normalized }))
|
|
304
|
+
animationId = requestAnimationFrame(updateIntensity)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
audio.onended = () => {
|
|
308
|
+
window.isMarkSpeaking = false
|
|
309
|
+
if (currentAudioElement === audio) {
|
|
310
|
+
currentAudioElement = null
|
|
311
|
+
}
|
|
312
|
+
window.dispatchEvent(new CustomEvent('mark-intensity', { detail: 0 }))
|
|
313
|
+
if (animationId) cancelAnimationFrame(animationId)
|
|
314
|
+
if (onEnd) onEnd()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
audio.onerror = (e) => {
|
|
318
|
+
console.warn('[playVoice] Audio playback warning:', e)
|
|
319
|
+
window.isMarkSpeaking = false
|
|
320
|
+
if (currentAudioElement === audio) {
|
|
321
|
+
currentAudioElement = null
|
|
322
|
+
}
|
|
323
|
+
window.dispatchEvent(new CustomEvent('mark-intensity', { detail: 0 }))
|
|
324
|
+
if (animationId) cancelAnimationFrame(animationId)
|
|
325
|
+
if (onEnd) onEnd()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 3. Mainkan audio seketika
|
|
329
|
+
window.isMarkSpeaking = true
|
|
330
|
+
await audio.play()
|
|
331
|
+
if (analyser) updateIntensity()
|
|
332
|
+
if (onStart) onStart()
|
|
333
|
+
} else {
|
|
334
|
+
if (onStart) onStart()
|
|
335
|
+
if (onEnd) onEnd()
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error('Gagal memutar suara:', error)
|
|
339
|
+
if (window.api && window.api.showNotification) {
|
|
340
|
+
window.api.showNotification('Error TTS', String(error.message || error))
|
|
341
|
+
}
|
|
342
|
+
window.isMarkSpeaking = false
|
|
343
|
+
window.dispatchEvent(new CustomEvent('mark-intensity', { detail: 0 }))
|
|
344
|
+
if (onStart) onStart()
|
|
345
|
+
if (onEnd) onEnd()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ==========================================
|
|
350
|
+
// TELEGRAM UTILS
|
|
351
|
+
// ==========================================
|
|
352
|
+
export const formatForTelegram = (text) => {
|
|
353
|
+
if (!text) return ''
|
|
354
|
+
return text.trim()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ==========================================
|
|
358
|
+
// PLANNING (AGENTIC) FUNCTIONS
|
|
359
|
+
// ==========================================
|