20251125 save
This commit is contained in:
parent
922fa0e77a
commit
092f2ec0f3
11 changed files with 299 additions and 1 deletions
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -11,4 +11,12 @@ venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.csv
|
*.csv
|
||||||
|
|
||||||
request.json
|
request.json
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env_dev
|
||||||
|
.env
|
||||||
|
.env_prod
|
||||||
|
credentials.json
|
||||||
|
package-lock.json
|
||||||
16
functions/generate_minutes/index.ts
Normal file
16
functions/generate_minutes/index.ts
Normal file
|
|
@ -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!`);
|
||||||
|
// };
|
||||||
31
functions/generate_minutes/package.json
Normal file
31
functions/generate_minutes/package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
functions/generate_minutes/src/apiRouter.ts
Normal file
35
functions/generate_minutes/src/apiRouter.ts
Normal file
|
|
@ -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;
|
||||||
38
functions/generate_minutes/src/logics/ai.ts
Normal file
38
functions/generate_minutes/src/logics/ai.ts
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
25
functions/generate_minutes/src/logics/date.ts
Normal file
25
functions/generate_minutes/src/logics/date.ts
Normal file
|
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
36
functions/generate_minutes/src/logics/googleDrive.ts
Normal file
36
functions/generate_minutes/src/logics/googleDrive.ts
Normal file
|
|
@ -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<any> => {
|
||||||
|
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() => {
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
0
functions/generate_minutes/src/logics/hubspot.ts
Normal file
0
functions/generate_minutes/src/logics/hubspot.ts
Normal file
57
functions/generate_minutes/src/logics/process.ts
Normal file
57
functions/generate_minutes/src/logics/process.ts
Normal file
|
|
@ -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<typeof VideoInfoSchema>;
|
||||||
|
|
||||||
|
export const MiiTelWebhookSchema = z.object({
|
||||||
|
video: VideoInfoSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
// export type MiiTelWebhook = z.infer<typeof MiiTelWebhookSchema>;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
};
|
||||||
39
functions/generate_minutes/src/logics/storage.ts
Normal file
39
functions/generate_minutes/src/logics/storage.ts
Normal file
|
|
@ -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<string | null> => {
|
||||||
|
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<boolean> => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
13
functions/generate_minutes/tsconfig.json
Normal file
13
functions/generate_minutes/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
// "include": ["", "index.ts"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue