From 092f2ec0f32ce1748d62d35badc3aa27674e913f Mon Sep 17 00:00:00 2001 From: kosukesuenaga Date: Tue, 25 Nov 2025 14:54:01 +0900 Subject: [PATCH] 20251125 save --- .gitignore | 10 +++- functions/generate_minutes/index.ts | 16 ++++++ functions/generate_minutes/package.json | 31 ++++++++++ functions/generate_minutes/src/apiRouter.ts | 35 ++++++++++++ functions/generate_minutes/src/logics/ai.ts | 38 +++++++++++++ functions/generate_minutes/src/logics/date.ts | 25 ++++++++ .../src/logics/googleDrive.ts | 36 ++++++++++++ .../generate_minutes/src/logics/hubspot.ts | 0 .../generate_minutes/src/logics/process.ts | 57 +++++++++++++++++++ .../generate_minutes/src/logics/storage.ts | 39 +++++++++++++ functions/generate_minutes/tsconfig.json | 13 +++++ 11 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 functions/generate_minutes/index.ts create mode 100644 functions/generate_minutes/package.json create mode 100644 functions/generate_minutes/src/apiRouter.ts create mode 100644 functions/generate_minutes/src/logics/ai.ts create mode 100644 functions/generate_minutes/src/logics/date.ts create mode 100644 functions/generate_minutes/src/logics/googleDrive.ts create mode 100644 functions/generate_minutes/src/logics/hubspot.ts create mode 100644 functions/generate_minutes/src/logics/process.ts create mode 100644 functions/generate_minutes/src/logics/storage.ts create mode 100644 functions/generate_minutes/tsconfig.json diff --git a/.gitignore b/.gitignore index a9be4e2..2a32f03 100755 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,12 @@ venv/ __pycache__/ *.csv -request.json \ No newline at end of file +request.json + +node_modules/ +dist/ +.env_dev +.env +.env_prod +credentials.json +package-lock.json \ No newline at end of file diff --git a/functions/generate_minutes/index.ts b/functions/generate_minutes/index.ts new file mode 100644 index 0000000..45ac857 --- /dev/null +++ b/functions/generate_minutes/index.ts @@ -0,0 +1,16 @@ +// src/index.ts +import express from "express"; +import type { Express } from "express"; +import router from "./src/apiRouter"; + +const app: Express = express(); +app.use("/api", router); + +export const helloHttp = app; +// export const helloHttp = (req: Request, res: Response): void => { +// // console.log("Function invoked:", new Date().toISOString()); +// console.log("path:", req.path, "method:", req.method); + +// const name = (req.query.name as string) ?? "World"; +// res.status(200).send(`Hello, ${name} from TypeScript Cloud Functions!`); +// }; diff --git a/functions/generate_minutes/package.json b/functions/generate_minutes/package.json new file mode 100644 index 0000000..7de412b --- /dev/null +++ b/functions/generate_minutes/package.json @@ -0,0 +1,31 @@ +{ + "name": "generate_minutes", + "version": "1.0.0", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "npm run build && functions-framework --target=helloHttp --port=8080 --source=dist/index.js", + "dev": "dotenv -e .env_dev -- nodemon --watch . --exec \"functions-framework --target=helloHttp --port=8080\"", + "debug": "dotenv -e .env_dev -- node --inspect node_modules/.bin/functions-framework --source=dist/index.js --target=helloHttp", + "watch": "concurrently \"dotenv -e .env_dev -- npm run build -- --watch\" \"dotenv -e .env_dev -- nodemon --watch ./dist/ --exec npm run debug\"" + }, + "devDependencies": { + "@google-cloud/functions-framework": "^3.0.0", + "@types/express": "^4.17.0", + "@types/node": "^20.0.0", + "dotenv-cli": "^11.0.0", + "nodemon": "^3.1.11", + "ts-node": "^10.9.2", + "typescript": "^5.0.0" + }, + "dependencies": { + "@google-cloud/local-auth": "^2.1.0", + "@google-cloud/storage": "^7.17.3", + "@google/genai": "^1.30.0", + "concurrently": "^9.2.1", + "dotenv": "^17.2.3", + "express": "^4.21.2", + "googleapis": "^105.0.0", + "zod": "^4.1.13" + } +} diff --git a/functions/generate_minutes/src/apiRouter.ts b/functions/generate_minutes/src/apiRouter.ts new file mode 100644 index 0000000..cf8429c --- /dev/null +++ b/functions/generate_minutes/src/apiRouter.ts @@ -0,0 +1,35 @@ +import express from "express"; +import { storageController } from "./logics/storage"; +import { MiiTelWebhookSchema, processRequest } from "./logics/process"; + +const router = express.Router(); + +router.get("/hello", (req, res) => res.send("こんにちは!")); + +router.post("/miitel", async(req, res) => { + const body = req.body; + // await storageController.saveToGCS("request_log",'test', JSON.stringify(req.body)); + + const parsedBody = MiiTelWebhookSchema.safeParse(body); + if(!parsedBody.success) { + console.error("Invalid webhook body:", parsedBody.error); + return; + } + console.log("miitel webhook received:", parsedBody.data.video.id); + + await processRequest(parsedBody.data.video); + + res.send("こんにちは!"); +}); + +router.post("/getLog", async(req, res) => { + console.log(req.body); + const meetingId = req.body.meetingId; + const exist = await storageController.existsInGCS("request_log", "test.json.gz"); + console.log("Log exists:", exist); + const log = await storageController.loadFromGCS("request_log", meetingId + ".json.gz"); + console.log(log) + res.send(log); +}); + +export default router; \ No newline at end of file diff --git a/functions/generate_minutes/src/logics/ai.ts b/functions/generate_minutes/src/logics/ai.ts new file mode 100644 index 0000000..ee37ec6 --- /dev/null +++ b/functions/generate_minutes/src/logics/ai.ts @@ -0,0 +1,38 @@ +import { GoogleGenAI } from "@google/genai"; + + +const aiClient = new GoogleGenAI({ + apiKey: process.env.GEMINI_API_KEY, +}); + +export const aiController = { + generateMinutes: async(text: string) => { + const prompt = ` + あなたは議事録作成のプロフェッショナルです。以下の「文字起こし結果」は営業マンが録音した商談の文字起こしです。以下の制約条件に従い、最高の商談報告の議事録を作成してください。 + + 制約条件: + 1. 文字起こし結果にはAIによる書き起こしミスがある可能性を考慮してください。 + 2. 冒頭に主要な「決定事項」と「アクションアイテム」をまとめてください。 + 3. 議論のポイントを議題ごとに要約してください。 + 4. 見出しや箇条書きを用いて、情報が探しやすい構造で簡潔かつ明瞭に記述してください。 + 5. 要約は500文字以内に収めてください。 + 6. 箇条書き形式で簡潔にまとめてください。 + 7. マークダウン記法は使わず、各項目を「■」や「・」等を使って見やすくしてください。 + + 文字起こし結果: + ${text} + ` + + try { + const response = await aiClient.models.generateContent({ + model: process.env.GEMINI_MODEL_ID || "gemini-2.5-flash", + contents: prompt, + }) + console.log("AI Response:", response.text); + return response.text; + } catch (error) { + console.error("AI Generation Error:", error); + return null; + } + } +}; \ No newline at end of file diff --git a/functions/generate_minutes/src/logics/date.ts b/functions/generate_minutes/src/logics/date.ts new file mode 100644 index 0000000..a7461c1 --- /dev/null +++ b/functions/generate_minutes/src/logics/date.ts @@ -0,0 +1,25 @@ + +export const dateController = { + convertToJst: (date: string): Date => { + const utcDate = new Date(date); + const jstDate = utcDate.toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' }) + return new Date(jstDate); + }, + getFormattedDate: (date: Date, format: string): string => { + const symbol = { + M: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + m: date.getMinutes(), + s: date.getSeconds(), + }; + + const formatted = format.replace(/(M+|d+|h+|m+|s+)/g, (v) => + ((v.length > 1 ? "0" : "") + symbol[v.slice(-1) as keyof typeof symbol]).slice(-2) + ); + + return formatted.replace(/(y+)/g, (v) => + date.getFullYear().toString().slice(-v.length) + ); + } +}; \ No newline at end of file diff --git a/functions/generate_minutes/src/logics/googleDrive.ts b/functions/generate_minutes/src/logics/googleDrive.ts new file mode 100644 index 0000000..bf13ea6 --- /dev/null +++ b/functions/generate_minutes/src/logics/googleDrive.ts @@ -0,0 +1,36 @@ +import { authenticate } from "@google-cloud/local-auth"; +import { JSONClient } from "google-auth-library/build/src/auth/googleauth"; +import { google } from "googleapis"; +import path from "path"; + +const SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file"] +const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json'); + +export const googleDriveController = { + getAuth: async():Promise => { + const auth = await new google.auth.GoogleAuth({ + keyFile: CREDENTIALS_PATH, + scopes: SCOPES, + }); + return auth; + }, + checkConnection: async() => { + const auth = await googleDriveController.getAuth(); + // console.log("Google Drive client authenticated."); + const drive = google.drive({ version: "v3", auth: auth}); + const folder = '1cCDJKusfrlDrJe2yHCR8pCHJXRqX-4Hw'; + const res = await drive.files.list({ + q: `'${folder}' in parents`, + pageSize: 10, + fields: "files(id, name)", + }); + console.log("Files:"); + console.log(res.data.files); + }, + uploadFile: async() => { + + }, + createNewFile: async() => { + + }, +}; \ No newline at end of file diff --git a/functions/generate_minutes/src/logics/hubspot.ts b/functions/generate_minutes/src/logics/hubspot.ts new file mode 100644 index 0000000..e69de29 diff --git a/functions/generate_minutes/src/logics/process.ts b/functions/generate_minutes/src/logics/process.ts new file mode 100644 index 0000000..b52d62a --- /dev/null +++ b/functions/generate_minutes/src/logics/process.ts @@ -0,0 +1,57 @@ +import z from "zod"; +import { aiController } from "./ai"; +import { dateController } from "./date"; +import { googleDriveController } from "./googleDrive"; + +const VideoInfoSchema = z.looseObject({ + id: z.string(), + title: z.string(), + starts_at: z.string(), + ends_at: z.string(), + access_permission: z.string(), + host: z.object({ + login_id: z.string(), + user_name: z.string(), + }), + speech_recognition: z.object({ + raw: z.string(), + }) +}); + +type VideoInfo = z.infer; + +export const MiiTelWebhookSchema = z.object({ + video: VideoInfoSchema, +}); + +// export type MiiTelWebhook = z.infer; + +export const processRequest = async(videoInfo: VideoInfo) => { + const videoId = videoInfo.id; + const title = videoInfo.title; + const startsAt = videoInfo.starts_at; + const endsAt = videoInfo.ends_at; + const accessPermission = videoInfo.access_permission; + + const host_id = videoInfo.host.login_id; + const host_name = videoInfo.host.user_name; + + const speechRecognition = videoInfo.speech_recognition.raw; + + console.log(startsAt); + const jstStartsAt = dateController.convertToJst(startsAt); + const jstEndsAt = dateController.convertToJst(endsAt); + + googleDriveController.checkConnection(); + // console.log(dateController.getFormattedDate(startsAtJst, "yyyy/MM/dd hh:mm:ss")); + // console.log(endsAt); + // console.log("Processing video:", host_id, host_name, title); + if(accessPermission !== "EVERYONE" || !title.includes("様") || title.includes("社内")) return; + + + + // Save Request Log to Google Drive + // const minute = await aiController.generateMinutes(speechRecognition); + // console.log(minute); + + }; \ No newline at end of file diff --git a/functions/generate_minutes/src/logics/storage.ts b/functions/generate_minutes/src/logics/storage.ts new file mode 100644 index 0000000..31b14df --- /dev/null +++ b/functions/generate_minutes/src/logics/storage.ts @@ -0,0 +1,39 @@ +import { Storage } from "@google-cloud/storage"; +import zlib from "zlib"; + +const csClient = new Storage({ + projectId: 'datacom-poc', +} +); +const BUCKET_NAME = "meeting-report-data"; +const bucket = csClient.bucket(BUCKET_NAME); + +export const storageController = { + saveToGCS: async(folder: string, filename: string, text: string) => { + const gzipped = zlib.gzipSync(text); + const file = bucket.file((`${folder}/${filename}.json.gz`)); + await file.save(gzipped, { + contentType: 'application/gzip', + }) + }, + loadFromGCS: async(folder: string, filename: string): Promise => { + const file = bucket.file(`${folder}/${filename}`); + // console.log("loading file:", file.name); + try { + const [data] = await file.download(); + return zlib.gunzipSync(data).toString("utf-8"); + } catch (err: any) { + return null; + } + }, + existsInGCS: async(folder: string, filename: string): Promise => { + const file = bucket.file((`${folder}/${filename}`)); + console.log("checking file:", file.name); + try { + const [exist] = await file.exists(); + return exist; + } catch (err: any) { + return false; + } + }, +}; diff --git a/functions/generate_minutes/tsconfig.json b/functions/generate_minutes/tsconfig.json new file mode 100644 index 0000000..1089c10 --- /dev/null +++ b/functions/generate_minutes/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "skipLibCheck": true + }, + // "include": ["", "index.ts"] +}