20251125 save

This commit is contained in:
kosukesuenaga 2025-11-25 14:54:01 +09:00
parent 922fa0e77a
commit 092f2ec0f3
11 changed files with 299 additions and 1 deletions

10
.gitignore vendored
View file

@ -11,4 +11,12 @@ venv/
__pycache__/
*.csv
request.json
request.json
node_modules/
dist/
.env_dev
.env
.env_prod
credentials.json
package-lock.json

View 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!`);
// };

View 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"
}
}

View 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;

View 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;
}
}
};

View 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)
);
}
};

View 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() => {
},
};

View 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);
};

View 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;
}
},
};

View 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"]
}