@hasna/microservices 0.0.2 → 0.0.3

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 (26) hide show
  1. package/bin/index.js +7 -0
  2. package/bin/mcp.js +8 -1
  3. package/dist/index.js +7 -0
  4. package/microservices/microservice-transcriber/package.json +28 -0
  5. package/microservices/microservice-transcriber/src/cli/index.ts +1347 -0
  6. package/microservices/microservice-transcriber/src/db/annotations.ts +37 -0
  7. package/microservices/microservice-transcriber/src/db/database.ts +82 -0
  8. package/microservices/microservice-transcriber/src/db/migrations.ts +72 -0
  9. package/microservices/microservice-transcriber/src/db/transcripts.ts +395 -0
  10. package/microservices/microservice-transcriber/src/index.ts +43 -0
  11. package/microservices/microservice-transcriber/src/lib/config.ts +77 -0
  12. package/microservices/microservice-transcriber/src/lib/diff.ts +91 -0
  13. package/microservices/microservice-transcriber/src/lib/downloader.ts +570 -0
  14. package/microservices/microservice-transcriber/src/lib/feeds.ts +62 -0
  15. package/microservices/microservice-transcriber/src/lib/live.ts +94 -0
  16. package/microservices/microservice-transcriber/src/lib/notion.ts +129 -0
  17. package/microservices/microservice-transcriber/src/lib/providers.ts +713 -0
  18. package/microservices/microservice-transcriber/src/lib/summarizer.ts +147 -0
  19. package/microservices/microservice-transcriber/src/lib/translator.ts +75 -0
  20. package/microservices/microservice-transcriber/src/lib/webhook.ts +37 -0
  21. package/microservices/microservice-transcriber/src/mcp/index.ts +1070 -0
  22. package/microservices/microservice-transcriber/src/server/index.ts +199 -0
  23. package/package.json +1 -1
  24. package/microservices/microservice-invoices/dashboard/dist/assets/index-Bngq7FNM.css +0 -1
  25. package/microservices/microservice-invoices/dashboard/dist/assets/index-aHW4ARZR.js +0 -124
  26. package/microservices/microservice-invoices/dashboard/dist/index.html +0 -13
@@ -0,0 +1,147 @@
1
+ /**
2
+ * AI-powered transcript summarization.
3
+ * Supports OpenAI (gpt-4o-mini) and Anthropic (claude-haiku) — both cheap and fast.
4
+ * Default: OpenAI if OPENAI_API_KEY set, else Anthropic.
5
+ */
6
+
7
+ export type SummaryProvider = "openai" | "anthropic";
8
+
9
+ const SUMMARY_PROMPT = (text: string) =>
10
+ `Summarize the following transcript in 3-5 concise sentences. Focus on the main topics, key points, and conclusions. Do not include filler or meta-commentary.\n\nTranscript:\n${text.slice(0, 12000)}`;
11
+
12
+ export function getDefaultSummaryProvider(): SummaryProvider | null {
13
+ if (process.env["OPENAI_API_KEY"]) return "openai";
14
+ if (process.env["ANTHROPIC_API_KEY"]) return "anthropic";
15
+ return null;
16
+ }
17
+
18
+ export async function summarizeText(
19
+ text: string,
20
+ provider?: SummaryProvider
21
+ ): Promise<string> {
22
+ const resolved = provider ?? getDefaultSummaryProvider();
23
+ if (!resolved) throw new Error("No summarization API key found. Set OPENAI_API_KEY or ANTHROPIC_API_KEY.");
24
+
25
+ if (resolved === "openai") return callOpenAI(SUMMARY_PROMPT(text), 300);
26
+ return callAnthropic(SUMMARY_PROMPT(text), 300);
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Highlights extraction
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export interface Highlight {
34
+ quote: string;
35
+ speaker?: string;
36
+ context: string;
37
+ }
38
+
39
+ const HIGHLIGHTS_PROMPT = (text: string) =>
40
+ `Extract 5-10 key moments from this transcript. For each, provide the exact quote and a one-sentence context explaining why it's important. Return as a JSON array of objects with fields: "quote" (the exact words), "speaker" (if identifiable), "context" (why this matters).
41
+
42
+ Return ONLY valid JSON, no markdown or explanation.
43
+
44
+ Transcript:
45
+ ${text.slice(0, 12000)}`;
46
+
47
+ export async function extractHighlights(
48
+ text: string,
49
+ provider?: SummaryProvider
50
+ ): Promise<Highlight[]> {
51
+ const resolved = provider ?? getDefaultSummaryProvider();
52
+ if (!resolved) throw new Error("No AI API key found. Set OPENAI_API_KEY or ANTHROPIC_API_KEY.");
53
+
54
+ let raw: string;
55
+ if (resolved === "openai") {
56
+ raw = await callOpenAI(HIGHLIGHTS_PROMPT(text), 1500);
57
+ } else {
58
+ raw = await callAnthropic(HIGHLIGHTS_PROMPT(text), 1500);
59
+ }
60
+
61
+ // Parse JSON from response (may have markdown fences)
62
+ const cleaned = raw.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
63
+ try {
64
+ const parsed = JSON.parse(cleaned);
65
+ return Array.isArray(parsed) ? parsed : [];
66
+ } catch {
67
+ return [];
68
+ }
69
+ }
70
+
71
+ // Shared low-level callers
72
+ async function callOpenAI(prompt: string, maxTokens: number): Promise<string> {
73
+ const apiKey = process.env["OPENAI_API_KEY"];
74
+ if (!apiKey) throw new Error("OPENAI_API_KEY is not set");
75
+
76
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
77
+ method: "POST",
78
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
79
+ body: JSON.stringify({
80
+ model: "gpt-4o-mini",
81
+ messages: [{ role: "user", content: prompt }],
82
+ max_tokens: maxTokens,
83
+ temperature: 0.3,
84
+ }),
85
+ });
86
+
87
+ if (!res.ok) { const body = await res.text(); throw new Error(`OpenAI API error ${res.status}: ${body}`); }
88
+ const data = (await res.json()) as { choices: Array<{ message: { content: string } }> };
89
+ return data.choices[0]?.message?.content?.trim() ?? "";
90
+ }
91
+
92
+ async function callAnthropic(prompt: string, maxTokens: number): Promise<string> {
93
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
94
+ if (!apiKey) throw new Error("ANTHROPIC_API_KEY is not set");
95
+
96
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
97
+ method: "POST",
98
+ headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01", "Content-Type": "application/json" },
99
+ body: JSON.stringify({
100
+ model: "claude-haiku-4-5-20251001",
101
+ max_tokens: maxTokens,
102
+ messages: [{ role: "user", content: prompt }],
103
+ }),
104
+ });
105
+
106
+ if (!res.ok) { const body = await res.text(); throw new Error(`Anthropic API error ${res.status}: ${body}`); }
107
+ const data = (await res.json()) as { content: Array<{ type: string; text: string }> };
108
+ return data.content.find((b) => b.type === "text")?.text?.trim() ?? "";
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Meeting notes generation
113
+ // ---------------------------------------------------------------------------
114
+
115
+ const MEETING_NOTES_PROMPT = (text: string) =>
116
+ `Restructure the following transcript into formatted meeting notes in Markdown. Include these sections:
117
+
118
+ ## Attendees
119
+ List all speakers/participants identified in the transcript.
120
+
121
+ ## Agenda / Topics Discussed
122
+ Bullet points of main topics covered.
123
+
124
+ ## Key Decisions
125
+ Bullet points of any decisions made.
126
+
127
+ ## Action Items
128
+ Bullet points of tasks, responsibilities, or follow-ups mentioned.
129
+
130
+ ## Summary
131
+ 2-3 sentence overview of the meeting.
132
+
133
+ Be concise. Only include sections that have content. Use the actual speaker names if available.
134
+
135
+ Transcript:
136
+ ${text.slice(0, 12000)}`;
137
+
138
+ export async function generateMeetingNotes(
139
+ text: string,
140
+ provider?: SummaryProvider
141
+ ): Promise<string> {
142
+ const resolved = provider ?? getDefaultSummaryProvider();
143
+ if (!resolved) throw new Error("No AI API key found. Set OPENAI_API_KEY or ANTHROPIC_API_KEY.");
144
+
145
+ if (resolved === "openai") return callOpenAI(MEETING_NOTES_PROMPT(text), 2000);
146
+ return callAnthropic(MEETING_NOTES_PROMPT(text), 2000);
147
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * AI-powered transcript translation.
3
+ * Supports OpenAI (gpt-4o-mini) and Anthropic (claude-haiku) — reuses the same
4
+ * provider detection as summarizer. Default: OpenAI if key set, else Anthropic.
5
+ */
6
+
7
+ import { getDefaultSummaryProvider, type SummaryProvider } from "./summarizer.js";
8
+
9
+ export { getDefaultSummaryProvider as getDefaultTranslationProvider };
10
+
11
+ const TRANSLATE_PROMPT = (text: string, targetLang: string) =>
12
+ `Translate the following transcript to ${targetLang}. Preserve the original structure and speaker labels if present. Output only the translated text, no explanations.\n\nTranscript:\n${text.slice(0, 12000)}`;
13
+
14
+ export async function translateText(
15
+ text: string,
16
+ targetLang: string,
17
+ provider?: SummaryProvider
18
+ ): Promise<string> {
19
+ const resolved = provider ?? getDefaultSummaryProvider();
20
+ if (!resolved) throw new Error("No translation API key found. Set OPENAI_API_KEY or ANTHROPIC_API_KEY.");
21
+
22
+ if (resolved === "openai") return translateWithOpenAI(text, targetLang);
23
+ return translateWithAnthropic(text, targetLang);
24
+ }
25
+
26
+ async function translateWithOpenAI(text: string, targetLang: string): Promise<string> {
27
+ const apiKey = process.env["OPENAI_API_KEY"];
28
+ if (!apiKey) throw new Error("OPENAI_API_KEY is not set");
29
+
30
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
31
+ method: "POST",
32
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
33
+ body: JSON.stringify({
34
+ model: "gpt-4o-mini",
35
+ messages: [{ role: "user", content: TRANSLATE_PROMPT(text, targetLang) }],
36
+ max_tokens: 4096,
37
+ temperature: 0.2,
38
+ }),
39
+ });
40
+
41
+ if (!res.ok) {
42
+ const body = await res.text();
43
+ throw new Error(`OpenAI API error ${res.status}: ${body}`);
44
+ }
45
+
46
+ const data = (await res.json()) as { choices: Array<{ message: { content: string } }> };
47
+ return data.choices[0]?.message?.content?.trim() ?? "";
48
+ }
49
+
50
+ async function translateWithAnthropic(text: string, targetLang: string): Promise<string> {
51
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
52
+ if (!apiKey) throw new Error("ANTHROPIC_API_KEY is not set");
53
+
54
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
55
+ method: "POST",
56
+ headers: {
57
+ "x-api-key": apiKey,
58
+ "anthropic-version": "2023-06-01",
59
+ "Content-Type": "application/json",
60
+ },
61
+ body: JSON.stringify({
62
+ model: "claude-haiku-4-5-20251001",
63
+ max_tokens: 4096,
64
+ messages: [{ role: "user", content: TRANSLATE_PROMPT(text, targetLang) }],
65
+ }),
66
+ });
67
+
68
+ if (!res.ok) {
69
+ const body = await res.text();
70
+ throw new Error(`Anthropic API error ${res.status}: ${body}`);
71
+ }
72
+
73
+ const data = (await res.json()) as { content: Array<{ type: string; text: string }> };
74
+ return data.content.find((b) => b.type === "text")?.text?.trim() ?? "";
75
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Webhook notifications — fires a POST to a configured URL after transcription events.
3
+ */
4
+
5
+ import { getConfig } from "./config.js";
6
+
7
+ export interface WebhookPayload {
8
+ event: "transcription.completed" | "transcription.failed";
9
+ id: string;
10
+ title: string | null;
11
+ status: string;
12
+ source_url: string | null;
13
+ provider: string;
14
+ duration_seconds: number | null;
15
+ word_count: number | null;
16
+ timestamp: string;
17
+ }
18
+
19
+ /**
20
+ * Fire a webhook if configured. Silently ignores failures — webhooks are best-effort.
21
+ */
22
+ export async function fireWebhook(payload: WebhookPayload): Promise<void> {
23
+ const cfg = getConfig();
24
+ const url = (cfg as Record<string, unknown>)["webhook"] as string | undefined;
25
+ if (!url) return;
26
+
27
+ try {
28
+ await fetch(url, {
29
+ method: "POST",
30
+ headers: { "Content-Type": "application/json" },
31
+ body: JSON.stringify(payload),
32
+ signal: AbortSignal.timeout(10000), // 10s timeout
33
+ });
34
+ } catch {
35
+ // Webhooks are fire-and-forget — don't fail the transcription
36
+ }
37
+ }