samuraizer 0.0.1

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.
Files changed (47) hide show
  1. package/README.md +206 -0
  2. package/dist/checks/ffmpeg.js +9 -0
  3. package/dist/checks/ffprobe.js +9 -0
  4. package/dist/checks/ollama.js +12 -0
  5. package/dist/checks/whisper.js +9 -0
  6. package/dist/cli/commands/process.js +138 -0
  7. package/dist/cli/index.js +175 -0
  8. package/dist/cli/process-command.js +21 -0
  9. package/dist/config/defaults.js +7 -0
  10. package/dist/config/init.js +21 -0
  11. package/dist/config/load.js +57 -0
  12. package/dist/config/paths.js +20 -0
  13. package/dist/config/schema.js +12 -0
  14. package/dist/config/template.js +9 -0
  15. package/dist/config/types.js +1 -0
  16. package/dist/dev/run-tool.js +38 -0
  17. package/dist/dev/runProcessMeeting.js +21 -0
  18. package/dist/infra/ffmpeg/run-ffmpeg.js +29 -0
  19. package/dist/infra/ollama/ollama-client.js +42 -0
  20. package/dist/infra/whisper/run-whisper-cli.js +40 -0
  21. package/dist/lib/ollama.js +51 -0
  22. package/dist/lib/run-command.js +8 -0
  23. package/dist/orchestrators/process-meeting.js +196 -0
  24. package/dist/pipeline/analysis/action-items/generate.js +117 -0
  25. package/dist/pipeline/analysis/action-items/types.js +1 -0
  26. package/dist/pipeline/analysis/decisions/generate.js +101 -0
  27. package/dist/pipeline/analysis/decisions/types.js +1 -0
  28. package/dist/pipeline/analysis/summary/generate.js +24 -0
  29. package/dist/pipeline/analysis/summary/types.js +1 -0
  30. package/dist/pipeline/audio/normalize.js +11 -0
  31. package/dist/pipeline/audio/probe.js +37 -0
  32. package/dist/pipeline/audio/validate-input.js +46 -0
  33. package/dist/pipeline/output/paths.js +19 -0
  34. package/dist/pipeline/output/prepare.js +22 -0
  35. package/dist/pipeline/output/save.js +16 -0
  36. package/dist/pipeline/report/generate.js +54 -0
  37. package/dist/pipeline/transcription/transcribe.js +36 -0
  38. package/dist/pipeline/transcription/types.js +1 -0
  39. package/dist/shared/tool-definition.js +5 -0
  40. package/dist/shared/tool-registry.js +12 -0
  41. package/dist/tools/analysis/action-item-types.js +1 -0
  42. package/dist/tools/analysis/extract-action-items-tool.js +94 -0
  43. package/dist/tools/analysis/extract-decisions-tool.js +84 -0
  44. package/dist/tools/analysis/summarize-transcription-tool.js +44 -0
  45. package/dist/tools/input/normalize-audio-tool.js +24 -0
  46. package/dist/tools/transcription/transcribe-audio-tool.js +43 -0
  47. package/package.json +42 -0
@@ -0,0 +1,101 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import z from "zod";
3
+ import { callOllama } from "../../../lib/ollama.js";
4
+ const decisionItemSchema = z.object({
5
+ text: z.string().min(1),
6
+ });
7
+ const decisionsResponseSchema = z.object({
8
+ items: z.array(decisionItemSchema),
9
+ });
10
+ const decisionsJsonSchema = {
11
+ type: "object",
12
+ properties: {
13
+ items: {
14
+ type: "array",
15
+ items: {
16
+ type: "object",
17
+ properties: {
18
+ text: { type: "string" },
19
+ },
20
+ required: ["text"],
21
+ additionalProperties: false,
22
+ },
23
+ },
24
+ },
25
+ required: ["items"],
26
+ additionalProperties: false,
27
+ };
28
+ export async function generateDecisionsWithOllama({ transcript, model, decisionsTextPath, sourceTranscriptPath, }) {
29
+ const prompt = buildDecisionsPrompt(transcript);
30
+ const rawText = await callOllama({ model, prompt, format: decisionsJsonSchema, temperature: 0 });
31
+ let parsedJson;
32
+ try {
33
+ parsedJson = JSON.parse(rawText);
34
+ }
35
+ catch {
36
+ throw new Error(`Failed to parse decisions JSON.\nRaw response:\n${rawText}`);
37
+ }
38
+ parsedJson = normalizeDecisionsJson(parsedJson);
39
+ const validated = decisionsResponseSchema.safeParse(parsedJson);
40
+ if (!validated.success) {
41
+ throw new Error(`Decisions JSON does not match schema.\n${validated.error.message}\nRaw response:\n${rawText}`);
42
+ }
43
+ const items = validated.data.items.filter((item) => item.text.trim().length > 0);
44
+ await writeFile(decisionsTextPath, toDecisionsText(items), "utf-8");
45
+ return { items, model, sourceTranscriptPath, createdAt: new Date().toISOString() };
46
+ }
47
+ function buildDecisionsPrompt(transcript) {
48
+ return `
49
+ Extract only final decisions from the meeting transcript.
50
+
51
+ Return ONLY valid JSON.
52
+ Do not return markdown.
53
+ Do not return explanations.
54
+ Do not return prose.
55
+ Do not return keys other than "items".
56
+
57
+ Expected format:
58
+ {
59
+ "items": [
60
+ { "text": "Decision 1" },
61
+ { "text": "Decision 2" }
62
+ ]
63
+ }
64
+
65
+ Rules:
66
+ - Include only decisions that were actually agreed or confirmed.
67
+ - Do not include action items.
68
+ - Do not include discussion points.
69
+ - Do not include open questions.
70
+ - If there are no decisions, return exactly:
71
+ {"items":[]}
72
+
73
+ Transcript:
74
+ ${transcript}
75
+ `.trim();
76
+ }
77
+ function toDecisionsText(items) {
78
+ if (items.length === 0)
79
+ return "No decisions found.";
80
+ return items.map((item) => `- ${item.text}`).join("\n");
81
+ }
82
+ function normalizeDecisionsJson(input) {
83
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
84
+ throw new Error(`Decisions response is not an object: ${JSON.stringify(input)}`);
85
+ }
86
+ const obj = input;
87
+ if (!("items" in obj)) {
88
+ throw new Error(`Decisions response does not contain "items".\nRaw parsed JSON:\n${JSON.stringify(input, null, 2)}`);
89
+ }
90
+ if (!Array.isArray(obj.items)) {
91
+ throw new Error(`Decisions field "items" is not an array.\nRaw parsed JSON:\n${JSON.stringify(input, null, 2)}`);
92
+ }
93
+ return {
94
+ items: obj.items.map((item) => {
95
+ if (!item || typeof item !== "object" || Array.isArray(item))
96
+ return { text: "" };
97
+ const i = item;
98
+ return { text: typeof i.text === "string" ? i.text : "" };
99
+ }),
100
+ };
101
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { callOllama } from "../../../lib/ollama.js";
3
+ export async function generateSummaryWithOllama({ transcript, model, summaryTextPath, sourceTranscriptPath, }) {
4
+ const prompt = buildSummaryPrompt(transcript);
5
+ const summaryText = await callOllama({ model, prompt });
6
+ await writeFile(summaryTextPath, summaryText, "utf-8");
7
+ return {
8
+ summary: summaryText,
9
+ model,
10
+ sourceTranscriptPath,
11
+ createdAt: new Date().toISOString(),
12
+ };
13
+ }
14
+ export function buildSummaryPrompt(transcript) {
15
+ return [
16
+ "You are an assistant that summarizes meeting transcripts.",
17
+ "Write a concise and clear summary of the conversation.",
18
+ "Focus on the main discussion points, important context, and outcomes.",
19
+ "Do not invent facts that are not present in the transcript.",
20
+ "Keep the summary readable and structured as plain text.",
21
+ "Transcript:",
22
+ transcript,
23
+ ].join("\n");
24
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { runCommand } from "../../lib/run-command.js";
2
+ export async function normalizeAudio({ inputPath, outputPath, ffmpegCommand }) {
3
+ const args = ["-y", "-i", inputPath, "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", outputPath];
4
+ try {
5
+ await runCommand(ffmpegCommand, args);
6
+ }
7
+ catch (error) {
8
+ const message = error instanceof Error ? error.message : "Unknown ffmpeg error";
9
+ throw new Error(`Failed to normalize audio: ${message}`);
10
+ }
11
+ }
@@ -0,0 +1,37 @@
1
+ import { runCommand } from "../../lib/run-command.js";
2
+ export async function probeAudio(filePath, ffprobeCommand) {
3
+ const args = ["-v", "error", "-print_format", "json", "-show_format", "-show_streams", filePath];
4
+ let stdout;
5
+ try {
6
+ const result = await runCommand(ffprobeCommand, args);
7
+ stdout = result.stdout;
8
+ }
9
+ catch (error) {
10
+ const message = error instanceof Error ? error.message : "Unknown ffprobe error";
11
+ throw new Error(`Failed to probe audio metadata.\n${message}`);
12
+ }
13
+ let parsed;
14
+ try {
15
+ parsed = JSON.parse(stdout);
16
+ }
17
+ catch {
18
+ throw new Error("Failed to parse ffprobe JSON output.");
19
+ }
20
+ const audioStream = parsed.streams?.find((s) => s.codec_type === "audio");
21
+ return {
22
+ filePath,
23
+ formatName: parsed.format?.format_name,
24
+ durationSec: toNumber(parsed.format?.duration),
25
+ sizeBytes: toNumber(parsed.format?.size),
26
+ bitRate: toNumber(parsed.format?.bit_rate),
27
+ sampleRate: toNumber(audioStream?.sample_rate),
28
+ channels: audioStream?.channels,
29
+ codecName: audioStream?.codec_name,
30
+ };
31
+ }
32
+ function toNumber(value) {
33
+ if (!value)
34
+ return undefined;
35
+ const parsed = Number(value);
36
+ return Number.isFinite(parsed) ? parsed : undefined;
37
+ }
@@ -0,0 +1,46 @@
1
+ import { access, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { constants } from "node:fs";
4
+ export const SUPPORTED_AUDIO_EXTENSIONS = [
5
+ ".mp3",
6
+ ".wav",
7
+ ".ogg",
8
+ ".flac",
9
+ ".m4a",
10
+ ".mp4",
11
+ ".aac",
12
+ ];
13
+ export async function validateInputFile(inputPath) {
14
+ if (!inputPath || !inputPath.trim())
15
+ throw new Error("No input file provided.");
16
+ const resolvedPath = path.resolve(inputPath);
17
+ const extension = path.extname(resolvedPath).toLowerCase();
18
+ if (!isSupportedExtension(extension)) {
19
+ throw new Error([
20
+ `Unsupported file format: ${extension || "(no extension)"}`,
21
+ `Supported formats: ${SUPPORTED_AUDIO_EXTENSIONS.join(", ")}`,
22
+ ].join("\n"));
23
+ }
24
+ try {
25
+ await access(resolvedPath, constants.F_OK);
26
+ }
27
+ catch {
28
+ throw new Error(`File does not exist: ${resolvedPath}`);
29
+ }
30
+ try {
31
+ await stat(resolvedPath);
32
+ }
33
+ catch {
34
+ throw new Error(`Path is not a file: ${resolvedPath}`);
35
+ }
36
+ return {
37
+ inputPath,
38
+ resolvedPath,
39
+ fileName: path.basename(resolvedPath),
40
+ baseName: path.basename(resolvedPath, extension),
41
+ extension,
42
+ };
43
+ }
44
+ function isSupportedExtension(extension) {
45
+ return SUPPORTED_AUDIO_EXTENSIONS.includes(extension);
46
+ }
@@ -0,0 +1,19 @@
1
+ import path from "node:path";
2
+ export function buildOutputPaths(inputFile, outputRootDir = path.resolve("output")) {
3
+ const runDir = path.join(outputRootDir, inputFile.baseName);
4
+ return {
5
+ outputRootDir,
6
+ runDir,
7
+ metaFilePath: path.join(runDir, "meta.json"),
8
+ normalizedAudioPath: path.join(runDir, "normalized.wav"),
9
+ transcriptTextPath: path.join(runDir, "transcript.txt"),
10
+ transcriptJsonPath: path.join(runDir, "transcript.json"),
11
+ summaryTextPath: path.join(runDir, "summary.txt"),
12
+ summaryJsonPath: path.join(runDir, "summary.json"),
13
+ actionItemsTextPath: path.join(runDir, "action-items.txt"),
14
+ actionItemsJsonPath: path.join(runDir, "action-items.json"),
15
+ decisionsTextPath: path.join(runDir, "decisions.txt"),
16
+ decisionsJsonPath: path.join(runDir, "decisions.json"),
17
+ reportMarkdownPath: path.join(runDir, "report.md"),
18
+ };
19
+ }
@@ -0,0 +1,22 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { buildOutputPaths } from "./paths.js";
3
+ export async function prepareOutput(inputFile, outputRootDir) {
4
+ const paths = buildOutputPaths(inputFile, outputRootDir);
5
+ await mkdir(paths.runDir, { recursive: true });
6
+ const meta = {
7
+ createdAt: new Date().toISOString(),
8
+ input: {
9
+ originalPath: inputFile.inputPath,
10
+ resolvedPath: inputFile.resolvedPath,
11
+ fileName: inputFile.fileName,
12
+ baseName: inputFile.baseName,
13
+ extension: inputFile.extension,
14
+ },
15
+ output: {
16
+ runDir: paths.runDir,
17
+ },
18
+ status: "initialized",
19
+ };
20
+ await writeFile(paths.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
21
+ return { paths, meta };
22
+ }
@@ -0,0 +1,16 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ export async function saveTranscript(path, transcript) {
3
+ await writeFile(path, JSON.stringify(transcript, null, 2), "utf-8");
4
+ }
5
+ export async function saveSummary(path, summary) {
6
+ await writeFile(path, JSON.stringify(summary, null, 2), "utf-8");
7
+ }
8
+ export async function saveActionItems(path, actionItems) {
9
+ await writeFile(path, JSON.stringify(actionItems, null, 2), "utf-8");
10
+ }
11
+ export async function saveDecisions(path, decisions) {
12
+ await writeFile(path, JSON.stringify(decisions, null, 2), "utf-8");
13
+ }
14
+ export async function saveMeta(paths, meta) {
15
+ await writeFile(paths.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
16
+ }
@@ -0,0 +1,54 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ export async function generateReport({ reportPath, meetingTitle, sourceFileName, transcript, summary, actionItems, decisions, }) {
3
+ const markdown = buildMarkdownReport({ meetingTitle, sourceFileName, transcript, summary, actionItems, decisions });
4
+ await writeFile(reportPath, markdown, "utf-8");
5
+ }
6
+ function buildMarkdownReport({ meetingTitle, sourceFileName, transcript, summary, actionItems, decisions, }) {
7
+ const lines = [];
8
+ lines.push(`# Meeting Report: ${esc(meetingTitle)}`);
9
+ lines.push("");
10
+ lines.push(`**Source file:** ${esc(sourceFileName)}`);
11
+ lines.push(`**Created at:** ${esc(summary.createdAt)}`);
12
+ lines.push(`**Summary model:** ${esc(summary.model)}`);
13
+ lines.push(`**Action items model:** ${esc(actionItems.model)}`);
14
+ lines.push(`**Decisions model:** ${esc(decisions.model)}`);
15
+ lines.push("");
16
+ lines.push("## Summary");
17
+ lines.push("");
18
+ lines.push(summary.summary.trim() || "No summary available.");
19
+ lines.push("");
20
+ lines.push("## Action Items");
21
+ lines.push("");
22
+ if (actionItems.items.length === 0) {
23
+ lines.push("No action items found.");
24
+ }
25
+ else {
26
+ for (const item of actionItems.items) {
27
+ const details = [
28
+ item.owner ? `owner: ${item.owner}` : null,
29
+ item.dueDate ? `due: ${item.dueDate}` : null,
30
+ ].filter(Boolean);
31
+ lines.push(details.length > 0 ? `- ${item.text} — ${details.join(", ")}` : `- ${item.text}`);
32
+ }
33
+ }
34
+ lines.push("");
35
+ lines.push("## Decisions");
36
+ lines.push("");
37
+ if (decisions.items.length === 0) {
38
+ lines.push("No decisions found.");
39
+ }
40
+ else {
41
+ for (const item of decisions.items) {
42
+ lines.push(`- ${item.text}`);
43
+ }
44
+ }
45
+ lines.push("");
46
+ lines.push("## Transcript");
47
+ lines.push("");
48
+ lines.push(transcript.text.trim() || "No transcript available.");
49
+ lines.push("");
50
+ return lines.join("\n");
51
+ }
52
+ function esc(value) {
53
+ return value.replace(/([*_`[\]])/g, "\\$1");
54
+ }
@@ -0,0 +1,36 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { runCommand } from "../../lib/run-command.js";
3
+ export async function transcribeWithWhisper({ audioPath, outputPrefix, modelPath, language, whisperCommand, }) {
4
+ const args = ["-m", modelPath, "-f", audioPath, "-otxt", "-of", outputPrefix, "-nt", "-l", language];
5
+ try {
6
+ await runCommand(whisperCommand, args);
7
+ }
8
+ catch (error) {
9
+ const message = error instanceof Error ? error.message : "Unknown whisper-cli error";
10
+ throw new Error(`Failed to transcribe audio.\n${message}`);
11
+ }
12
+ const transcriptPath = `${outputPrefix}.txt`;
13
+ let transcriptText;
14
+ try {
15
+ transcriptText = await readFile(transcriptPath, "utf-8");
16
+ }
17
+ catch {
18
+ throw new Error(`whisper-cli finished but transcript file was not found: ${transcriptPath}`);
19
+ }
20
+ const cleanedText = transcriptText.trim();
21
+ await writeFile(transcriptPath, cleanedText, "utf-8");
22
+ return {
23
+ text: cleanedText,
24
+ segments: parseSegmentsFromPlainText(cleanedText),
25
+ sourceAudioPath: audioPath,
26
+ };
27
+ }
28
+ function parseSegmentsFromPlainText(text) {
29
+ if (!text.trim())
30
+ return [];
31
+ return text
32
+ .split(/\n+/)
33
+ .map((line) => line.trim())
34
+ .filter(Boolean)
35
+ .map((line, index) => ({ startSec: index, endSec: index, text: line }));
36
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export async function runTool(tool, input) {
2
+ const parsedInput = tool.inputSchema.parse(input);
3
+ const result = await tool.execute(parsedInput);
4
+ return tool.outputSchema.parse(result);
5
+ }
@@ -0,0 +1,12 @@
1
+ import { extractActionItemsTool } from "../tools/analysis/extract-action-items-tool.js";
2
+ import { extractDecisionsTool } from "../tools/analysis/extract-decisions-tool.js";
3
+ import { summarizeTranscriptionTool } from "../tools/analysis/summarize-transcription-tool.js";
4
+ import { normalizeAudioTool } from "../tools/input/normalize-audio-tool.js";
5
+ import { transcribeAudioTool } from "../tools/transcription/transcribe-audio-tool.js";
6
+ export const tools = {
7
+ normalize_audio: normalizeAudioTool,
8
+ transcribe_audio: transcribeAudioTool,
9
+ summarize_transcript: summarizeTranscriptionTool,
10
+ extract_action_items: extractActionItemsTool,
11
+ extract_decisions: extractDecisionsTool,
12
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ import { callOllama } from "../../lib/ollama.js";
3
+ const actionItemSchema = z.object({
4
+ text: z.string().min(1),
5
+ owner: z.string().nullable(),
6
+ dueDate: z.string().nullable(),
7
+ });
8
+ const inputSchema = z.object({
9
+ transcriptText: z.string().min(1),
10
+ model: z.string().min(1),
11
+ ollamaBaseUrl: z.string().url(),
12
+ });
13
+ const outputSchema = z.object({
14
+ items: z.array(actionItemSchema),
15
+ });
16
+ const jsonSchema = {
17
+ type: "object",
18
+ properties: {
19
+ items: {
20
+ type: "array",
21
+ items: {
22
+ type: "object",
23
+ properties: {
24
+ text: { type: "string" },
25
+ owner: { type: ["string", "null"] },
26
+ dueDate: { type: ["string", "null"] },
27
+ },
28
+ required: ["text", "owner", "dueDate"],
29
+ additionalProperties: false,
30
+ },
31
+ },
32
+ },
33
+ required: ["items"],
34
+ additionalProperties: false,
35
+ };
36
+ function buildPrompt(transcript) {
37
+ return [
38
+ "Extract action items from the meeting transcript.",
39
+ "Return ONLY valid JSON matching the required schema.",
40
+ "Do not return markdown, explanations, or any text outside of JSON.",
41
+ "",
42
+ "An action item is a concrete task that someone must DO after the meeting.",
43
+ "Examples of action items: 'Fix the login bug', 'Send credentials to John', 'Schedule follow-up call'.",
44
+ "",
45
+ "Rules:",
46
+ "- Include ONLY tasks with a clear verb: fix, send, check, create, review, schedule, etc.",
47
+ "- Do NOT include compliments, thank-yous, summaries, or observations.",
48
+ "- Do NOT include things that were already done during the meeting.",
49
+ "- If no clear owner is mentioned, use null.",
50
+ "- If no due date is mentioned, use null.",
51
+ '- If there are no action items, return {"items":[]}.',
52
+ "",
53
+ "Transcript:",
54
+ transcript,
55
+ ].join("\n");
56
+ }
57
+ export const extractActionItemsTool = {
58
+ name: "extract_action_items",
59
+ description: "Extract action items from a meeting transcript",
60
+ inputSchema,
61
+ outputSchema,
62
+ async execute(input) {
63
+ const raw = await callOllama({
64
+ baseUrl: input.ollamaBaseUrl,
65
+ model: input.model,
66
+ messages: [
67
+ {
68
+ role: "system",
69
+ content: "You are a meeting analyst. Extract action items from transcripts. Return ONLY valid JSON. Do not ask questions. Do not offer further help."
70
+ },
71
+ {
72
+ role: "user",
73
+ content: buildPrompt(input.transcriptText),
74
+ }
75
+ ],
76
+ format: jsonSchema,
77
+ temperature: 0,
78
+ });
79
+ let parsed;
80
+ try {
81
+ parsed = JSON.parse(raw);
82
+ }
83
+ catch {
84
+ throw new Error(`Failed to parse action items JSON.\nRaw response:\n${raw}`);
85
+ }
86
+ const result = outputSchema.safeParse(parsed);
87
+ if (!result.success) {
88
+ throw new Error(`Action items response does not match schema.\n${result.error.message}\nRaw:\n${raw}`);
89
+ }
90
+ return {
91
+ items: result.data.items.filter((item) => item.text.trim().length > 0),
92
+ };
93
+ },
94
+ };
@@ -0,0 +1,84 @@
1
+ import { z } from "zod";
2
+ import { callOllama } from "../../lib/ollama.js";
3
+ const decisionItemSchema = z.object({
4
+ text: z.string().min(1),
5
+ });
6
+ const inputSchema = z.object({
7
+ transcriptText: z.string().min(1),
8
+ model: z.string().min(1),
9
+ ollamaBaseUrl: z.string().url(),
10
+ });
11
+ const outputSchema = z.object({
12
+ items: z.array(decisionItemSchema),
13
+ });
14
+ const jsonSchema = {
15
+ type: "object",
16
+ properties: {
17
+ items: {
18
+ type: "array",
19
+ items: {
20
+ type: "object",
21
+ properties: {
22
+ text: { type: "string" },
23
+ },
24
+ required: ["text"],
25
+ additionalProperties: false,
26
+ },
27
+ },
28
+ },
29
+ required: ["items"],
30
+ additionalProperties: false,
31
+ };
32
+ function buildPrompt(transcript) {
33
+ return [
34
+ "Extract only final decisions from the meeting transcript.",
35
+ "Return ONLY valid JSON matching the required schema.",
36
+ "Do not return markdown, explanations, or any text outside of JSON.",
37
+ "",
38
+ "Rules:",
39
+ "- Include only decisions that were actually agreed or confirmed.",
40
+ "- Do not include action items, discussion points, or open questions.",
41
+ '- If there are no decisions, return {"items":[]}.',
42
+ "",
43
+ "Transcript:",
44
+ transcript,
45
+ ].join("\n");
46
+ }
47
+ export const extractDecisionsTool = {
48
+ name: "extract_decisions",
49
+ description: "Extract confirmed decisions from a meeting transcript",
50
+ inputSchema,
51
+ outputSchema,
52
+ async execute(input) {
53
+ const raw = await callOllama({
54
+ baseUrl: input.ollamaBaseUrl,
55
+ model: input.model,
56
+ messages: [
57
+ {
58
+ role: "system",
59
+ content: "You are a meeting analyst. Extract action items from transcripts. Return ONLY valid JSON. Do not ask questions. Do not offer further help."
60
+ },
61
+ {
62
+ role: "user",
63
+ content: buildPrompt(input.transcriptText),
64
+ }
65
+ ],
66
+ format: jsonSchema,
67
+ temperature: 0,
68
+ });
69
+ let parsed;
70
+ try {
71
+ parsed = JSON.parse(raw);
72
+ }
73
+ catch {
74
+ throw new Error(`Failed to parse decisions JSON.\nRaw response:\n${raw}`);
75
+ }
76
+ const result = outputSchema.safeParse(parsed);
77
+ if (!result.success) {
78
+ throw new Error(`Decisions response does not match schema.\n${result.error.message}\nRaw:\n${raw}`);
79
+ }
80
+ return {
81
+ items: result.data.items.filter((item) => item.text.trim().length > 0),
82
+ };
83
+ },
84
+ };
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import { callOllama } from "../../lib/ollama.js";
3
+ const inputSchema = z.object({
4
+ transcriptText: z.string().min(1),
5
+ model: z.string().min(1),
6
+ ollamaBaseUrl: z.string().url(),
7
+ });
8
+ const outputSchema = z.object({
9
+ summary: z.string().min(1),
10
+ });
11
+ function buildPrompt(transcript) {
12
+ return [
13
+ "You are an assistant that summarizes meeting transcripts.",
14
+ "Write a concise and clear summary of the conversation.",
15
+ "Focus on the main discussion points, important context, and outcomes.",
16
+ "Do not invent facts that are not present in the transcript.",
17
+ "Keep the summary readable and structured as plain text.",
18
+ "Transcript:",
19
+ transcript,
20
+ ].join("\n");
21
+ }
22
+ export const summarizeTranscriptionTool = {
23
+ name: "summarize_transcript",
24
+ description: "Generate a meeting summary from transcript text",
25
+ inputSchema,
26
+ outputSchema,
27
+ async execute(input) {
28
+ const summary = await callOllama({
29
+ baseUrl: input.ollamaBaseUrl,
30
+ model: input.model,
31
+ messages: [
32
+ {
33
+ role: "system",
34
+ content: "You are a meeting analyst. Extract action items from transcripts. Return ONLY valid JSON. Do not ask questions. Do not offer further help."
35
+ },
36
+ {
37
+ role: "user",
38
+ content: buildPrompt(input.transcriptText),
39
+ }
40
+ ],
41
+ });
42
+ return { summary };
43
+ },
44
+ };
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { normalizeAudio } from "../../pipeline/audio/normalize.js";
3
+ const inputSchema = z.object({
4
+ inputPath: z.string().min(1),
5
+ outputPath: z.string().min(1),
6
+ ffmpegCommand: z.string().min(1),
7
+ });
8
+ const outputSchema = z.object({
9
+ normalizedAudioPath: z.string().min(1),
10
+ });
11
+ export const normalizeAudioTool = {
12
+ name: "normalize_audio",
13
+ description: "Normalize audio to 16kHz mono PCM WAV for Whisper",
14
+ inputSchema,
15
+ outputSchema,
16
+ async execute(input) {
17
+ await normalizeAudio({
18
+ inputPath: input.inputPath,
19
+ outputPath: input.outputPath,
20
+ ffmpegCommand: input.ffmpegCommand,
21
+ });
22
+ return { normalizedAudioPath: input.outputPath };
23
+ },
24
+ };