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__/
|
||||
*.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