From 28461146241580d657c9680898343cf28bf0e4a5 Mon Sep 17 00:00:00 2001 From: Kubosaka Date: Fri, 15 Nov 2024 01:10:04 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=E9=A1=9E=E4=BC=BC=E5=BA=A6=E3=81=AE?= =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3?= =?UTF-8?q?=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/app/api/image/route.ts | 108 ++++++++--------- app/src/app/api/minio/route.ts | 179 ++++++++++++++++++----------- app/src/app/camera/page.tsx | 115 +++++++++--------- app/src/functions/scoreRegister.ts | 55 +++++++++ app/src/functions/simirality.ts | 3 +- app/src/types/index.tsx | 23 +++- 6 files changed, 306 insertions(+), 177 deletions(-) create mode 100644 app/src/functions/scoreRegister.ts diff --git a/app/src/app/api/image/route.ts b/app/src/app/api/image/route.ts index dae8025..df461b2 100644 --- a/app/src/app/api/image/route.ts +++ b/app/src/app/api/image/route.ts @@ -4,74 +4,74 @@ import type { NextRequest } from "next/server"; import { OpenAI } from "openai"; type ResponseData = { - message: string; + message: string; }; const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, + apiKey: process.env.OPENAI_API_KEY, }); const BUCKET_NAME = "kz2404"; export async function GET( - req: NextRequest, - res: NextApiResponse + req: NextRequest, + res: NextApiResponse, ) { - // クエリパラメータを取得 + // クエリパラメータを取得 const { searchParams } = new URL(req.url || ""); const imageName = searchParams.get("imageName") || "default-image-name"; - const imageURL = `${process.env.NEXT_PUBLIC_MINIO_ENDPOINT}${BUCKET_NAME}/${imageName}`; + const imageURL = `${process.env.NEXT_PUBLIC_MINIO_ENDPOINT}${BUCKET_NAME}/${imageName}`; - return await generateCaption(imageURL) - .then((caption) => { - console.log(caption); - return new Response(JSON.stringify({ caption }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - }) - .catch((error) => { - console.error("Error generating caption:", error); - return new Response(JSON.stringify({ message: "エラーが発生しました" }), { - status: 500, - headers: { "Content-Type": "application/json" }, - }); - }); + return await generateCaption(imageURL) + .then((caption) => { + console.log(caption); + return new Response(JSON.stringify({ caption }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }) + .catch((error) => { + console.error("Error generating caption:", error); + return new Response(JSON.stringify({ message: "エラーが発生しました" }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + }); } // 画像URLからキャプションを生成する関数 -const generateCaption = async (imageUrl: string) => { - try { - const completion = await openai.chat.completions.create({ - model: "gpt-4o", - messages: [ - { - role: "system", - content: [ - { - type: "text", - text: "A robot that can generate very short captions for images.", - }, - ], - }, - { - role: "user", - content: [ - { - type: "image_url", - image_url: { - url: imageUrl, - detail: "low", - }, - }, - ], - }, - ], - }); +export const generateCaption = async (imageUrl: string) => { + try { + const completion = await openai.chat.completions.create({ + model: "gpt-4o", + messages: [ + { + role: "system", + content: [ + { + type: "text", + text: "A robot that can generate very short captions for images.", + }, + ], + }, + { + role: "user", + content: [ + { + type: "image_url", + image_url: { + url: imageUrl, + detail: "low", + }, + }, + ], + }, + ], + }); - const caption = await completion.choices[0].message.content; - return caption; - } catch (error) { - console.error("Error generating caption:", error); - } + const caption = await completion.choices[0].message.content; + return caption; + } catch (error) { + console.error("Error generating caption:", error); + } }; diff --git a/app/src/app/api/minio/route.ts b/app/src/app/api/minio/route.ts index f2f04b2..ec4ba05 100644 --- a/app/src/app/api/minio/route.ts +++ b/app/src/app/api/minio/route.ts @@ -1,76 +1,127 @@ -import fs from 'fs'; -import { formidable, IncomingForm } from 'formidable'; +import * as fs from "node:fs"; +import type { IncomingMessage } from "node:http"; +import { Readable } from "node:stream"; +import { scoreRegister } from "@/functions/scoreRegister"; +import { shapeCaption } from "@/functions/shapeCaption"; +import { postSimilarity } from "@/functions/simirality"; +import { prisma } from "@/lib/prisma"; +import type { ScoreData, ScoreResponse } from "@/types"; +import { formidable } from "formidable"; +import { Client } from "minio"; import type { NextRequest } from "next/server"; -import type { IncomingMessage } from 'http'; -import { Readable } from 'stream'; -import { Client } from 'minio'; +import { generateCaption } from "../image/route"; const minioClient = new Client({ - endPoint: process.env.NEXT_PUBLIC_ENDPOINT || '', - port: Number(process.env.NEXT_PUBLIC_PORT), - accessKey: process.env.NEXT_PUBLIC_ACCESS_KEY || '', - secretKey: process.env.NEXT_PUBLIC_SECRET_KEY || '', - useSSL: false, + endPoint: process.env.NEXT_PUBLIC_ENDPOINT || "", + port: Number(process.env.NEXT_PUBLIC_PORT), + accessKey: process.env.NEXT_PUBLIC_ACCESS_KEY || "", + secretKey: process.env.NEXT_PUBLIC_SECRET_KEY || "", + useSSL: false, }); -const BUCKET_NAME = 'kz2404'; +const BUCKET_NAME = "kz2404"; // NextRequestをIncomingMessageのようにラップする関数 function toIncomingMessage(request: NextRequest): IncomingMessage { - const readable = new Readable({ - read() { - request.body?.getReader().read().then(({ done, value }) => { - if (done) { - this.push(null); - } else { - this.push(Buffer.from(value)); - } - }); - }, - }); - - const msg = Object.assign(readable, { - headers: Object.fromEntries(request.headers), - method: request.method, - url: request.nextUrl.pathname, - }); - - return msg as IncomingMessage; -} + const readable = new Readable({ + read() { + request.body + ?.getReader() + .read() + .then(({ done, value }) => { + if (done) { + this.push(null); + } else { + this.push(Buffer.from(value)); + } + }); + }, + }); + + const msg = Object.assign(readable, { + headers: Object.fromEntries(request.headers), + method: request.method, + url: request.nextUrl.pathname, + }); + return msg as IncomingMessage; +} export async function POST(req: NextRequest) { - const form = formidable(); - const incomingMessage = toIncomingMessage(req); - - form.parse(incomingMessage, async (err: any, fields: any, files: any) => { - if (err) { - throw new Error('Error parsing form'); - } - const image = files.image[0]; - const mimetype = image.type; - const metaData = { - 'Content-Type': mimetype - }; - - try { - await minioClient.putObject( - BUCKET_NAME, - image.originalFilename, - fs.createReadStream(image.filepath), - image.size, - metaData, - ); - } catch (err) { - return new Response(JSON.stringify({ message: '失敗' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); - } - }); - - return new Response(JSON.stringify({ message: "成功" }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); + const form = formidable(); + const incomingMessage = toIncomingMessage(req); + + const { searchParams } = new URL(req.url || ""); + const fileName = searchParams.get("file") || ""; + const assignment = searchParams.get("assignment") || ""; + const uid = searchParams.get("uid") || ""; + const assignmentId = Number(searchParams.get("assignmentId") || 0); + + form.parse(incomingMessage, async (err: Error | null, _, files: any) => { + if (err || !files.image) { + throw new Error("Error parsing form"); + } + const image = files.image[0]; + const mimetype = image.type; + const metaData = { + "Content-Type": mimetype, + }; + + try { + await minioClient.putObject( + BUCKET_NAME, + image.originalFilename, + fs.createReadStream(image.filepath), + image.size, + metaData, + ); + } catch (err) { + return new Response(JSON.stringify({ message: "失敗" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + }); + + const imageURL = `${process.env.NEXT_PUBLIC_MINIO_ENDPOINT}${BUCKET_NAME}/${fileName}`; + + // キャプション生成 + const caption = await generateCaption(imageURL); + + // 類似度分割 + const words: string[] = shapeCaption(caption || ""); + + // 類似度計算 + const resSimilarity: { similarity: number } = await postSimilarity( + assignment, + words, + ); + + const user = await prisma.user.findUnique({ + where: { + uid: uid, + }, + }); + + const scoreData: ScoreData = { + similarity: resSimilarity.similarity, + answerTime: new Date(), + imageUrl: imageURL, + assignmentId: assignmentId, + userId: user?.id || 0, + }; + + const score = await scoreRegister(scoreData); + + const response: ScoreResponse = { + text: caption || "", + score: score?.point || 0, + similarity: resSimilarity.similarity, + assignmentId: assignmentId, + }; + + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); } diff --git a/app/src/app/camera/page.tsx b/app/src/app/camera/page.tsx index de2a027..f9f630f 100644 --- a/app/src/app/camera/page.tsx +++ b/app/src/app/camera/page.tsx @@ -1,5 +1,7 @@ "use client"; +import { Answered } from "@/components/Answered"; +import { AssignmentBadge } from "@/components/AssignmentBadge"; import { AlertDialog, AlertDialogAction, @@ -13,19 +15,17 @@ import { import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Toaster } from "@/components/ui/sonner"; import { shapeCaption } from "@/functions/shapeCaption"; import { postSimilarity } from "@/functions/simirality"; +import type { ScoreResponse, User, todayAssignment } from "@/types"; import type React from "react"; import { useEffect, useRef, useState } from "react"; import { Camera, type CameraType } from "react-camera-pro"; +import { toast } from "sonner"; import AddImageIcon from "../../../public/icons/icon-add-image.svg"; import RotateCameraIcon from "../../../public/icons/icon-rotate-camera.svg"; import ShutterIcon from "../../../public/icons/icon-shutter.svg"; -import { toast } from "sonner"; -import { Toaster } from "@/components/ui/sonner"; -import { todayAssignment } from "@/types"; -import { Answered } from "@/components/Answered"; -import { AssignmentBadge } from "@/components/AssignmentBadge"; interface ImagePreviewProps { image: string | null; @@ -100,11 +100,16 @@ const CameraApp = () => { const [tempImage, setTempImage] = useState(null); const camera = useRef(null); const [devices, setDevices] = useState([]); - const [activeDeviceId, setActiveDeviceId] = useState(undefined); + const [activeDeviceId, setActiveDeviceId] = useState( + undefined, + ); const [currentDeviceIndex, setCurrentDeviceIndex] = useState(0); - const [todayAssignment, setTodayAssignment] = useState(); + const [todayAssignment, setTodayAssignment] = useState< + todayAssignment | undefined + >(); const [assignments, setAssignments] = useState([]); const [isActive, setIsActive] = useState(true); + const [loginUser, setLoginUser] = useState(); useEffect(() => { const getDevices = async () => { @@ -114,7 +119,10 @@ const CameraApp = () => { return; } const userInfo = JSON.parse(user); - const resAssignment = await fetch(`/api/assignment/today?uid=${userInfo?.uid}`); + setLoginUser(userInfo); + const resAssignment = await fetch( + `/api/assignment/today?uid=${userInfo?.uid}`, + ); const assignmentData = await resAssignment.json(); if (assignmentData.length === 0) { @@ -143,7 +151,9 @@ const CameraApp = () => { try { const devices = await navigator.mediaDevices.enumerateDevices(); - const videoDevices = devices.filter((device) => device.kind === "videoinput"); + const videoDevices = devices.filter( + (device) => device.kind === "videoinput", + ); setDevices(videoDevices); if (videoDevices.length > 0) { setActiveDeviceId(videoDevices[0].deviceId); @@ -166,7 +176,7 @@ const CameraApp = () => { const uploadImage = async ( imageData: string, - ): Promise<{ imageName: string; data: UploadResponse }> => { + ): Promise<{ data: ScoreResponse }> => { setIsUploading(true); try { const base64Response = await fetch(imageData); @@ -185,7 +195,9 @@ const CameraApp = () => { // ランダム文字列を生成する関数 const generateRandomString = (charCount = 7): string => { const str = Math.random().toString(36).substring(2).slice(-charCount); - return str.length < charCount ? str + "a".repeat(charCount - str.length) : str; + return str.length < charCount + ? str + "a".repeat(charCount - str.length) + : str; }; const randomStr = generateRandomString(); @@ -195,21 +207,27 @@ const CameraApp = () => { const formData = new FormData(); formData.append("image", blob, imageName); - const response = await fetch("/api/minio", { - method: "POST", - body: formData, - }); + const response = await fetch( + `/api/minio?file=${imageName}&&assignment=${todayAssignment?.english}&&uid=${loginUser?.uid}&&assignmentId=${todayAssignment?.assignmentId}`, + { + method: "POST", + body: formData, + }, + ); const data = await response.json(); + console.log(data); - return { imageName, data }; + return { data }; } catch (error) { console.error("画像のアップロードに失敗しました:", error); throw error; } }; - const getCaption = async (imageName: string): Promise<{ caption: string }> => { + const getCaption = async ( + imageName: string, + ): Promise<{ caption: string }> => { try { const response = await fetch(`/api/image?imageName=${imageName}`); if (!response.ok) { @@ -236,13 +254,11 @@ const CameraApp = () => { // userIdの取得 const getUserId = async () => { - const userString = localStorage.getItem("userID"); - if (userString === null) { - return null; + if (!loginUser) { + console.error("ユーザー情報が取得できませんでした。"); + return; } - const userData = JSON.parse(userString); - - const resUserId = await fetch("/api/user?uid=" + userData.uid, { + const resUserId = await fetch("/api/user?uid=" + loginUser.uid, { method: "GET", headers: { "Content-Type": "application/json", @@ -272,39 +288,23 @@ const CameraApp = () => { const handleConfirm = async () => { if (tempImage) { try { - const { imageName } = await uploadImage(tempImage); - const imageURL = `${process.env.NEXT_PUBLIC_MINIO_ENDPOINT}${BUCKET_NAME}/${imageName}`; - - const res = await getCaption(imageName); - const caption = res.caption; + const { data } = await uploadImage(tempImage); setShowConfirmDialog(false); setImage(tempImage); setShowImage(true); setTempImage(null); - const { similarity, assignmentId } = await similarityRequest(caption); - - const user = await getUserId(); - const userId: number = user.id; - - const scoreData: ScoreData = { - similarity: similarity, - answerTime: new Date(), - imageUrl: imageURL, - assignmentId: assignmentId, - userId: userId, - }; - const response = await submitScore(scoreData); - const score = response.score; - const percentSimilarity = Math.floor(similarity * 100); - const message = `${caption} 類似度 ${percentSimilarity}% スコア: ${score.point} ランキングから順位を確認しましょう!`; + const percentSimilarity = Math.floor(data.similarity * 100); + + const message = `${data.text} 類似度 ${percentSimilarity}% スコア: ${data.score} ランキングから順位を確認しましょう!`; const newAssignments = assignments.map((assignment) => { - if (assignment.assignmentId === assignmentId) { + if (assignment.assignmentId === data.assignmentId) { assignment.isAnswered = true; } return assignment; }); + const notAnsweredAssignment = newAssignments.find( (assignment: todayAssignment) => !assignment.isAnswered, ); @@ -318,8 +318,6 @@ const CameraApp = () => { if (newAssignments.every((assignment) => assignment.isAnswered)) { setIsActive(false); } - - } catch (error) { setIsUploading(false); console.error("アップロード中にエラーが発生しました:", error); @@ -334,7 +332,9 @@ const CameraApp = () => { const handleImageCapture = (capturedImage: string | ImageData) => { const imageStr = - capturedImage instanceof ImageData ? imageDataToBase64(capturedImage) : capturedImage; + capturedImage instanceof ImageData + ? imageDataToBase64(capturedImage) + : capturedImage; setTempImage(imageStr); setShowConfirmDialog(true); @@ -409,10 +409,15 @@ const CameraApp = () => { - + - 画像のアップロード確認 + + 画像のアップロード確認 + この画像をアップロードしてもよろしいですか? @@ -421,8 +426,12 @@ const CameraApp = () => { - いいえ - はい + + いいえ + + + はい + @@ -437,7 +446,7 @@ const CameraApp = () => { )} ) : ( - + )} ); diff --git a/app/src/functions/scoreRegister.ts b/app/src/functions/scoreRegister.ts new file mode 100644 index 0000000..ab604a4 --- /dev/null +++ b/app/src/functions/scoreRegister.ts @@ -0,0 +1,55 @@ +import { prisma } from "@/lib/prisma"; +import type { Score, ScoreData } from "@/types"; + +const assignmentDate = async function GET() { + const assignment = await prisma.assignment.findFirst({ + select: { date: true }, + }); + const date = assignment?.date ?? null; + + return new Response(JSON.stringify({ date }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +}; + +export const scoreRegister = async (scoreData: ScoreData) => { + // 必須データの存在確認 + if ( + scoreData.similarity == null || + scoreData.answerTime == null || + scoreData.imageUrl == null || + scoreData.assignmentId == null || + scoreData.userId == null + ) { + return + } + + // ポイントの計算 + const minTime = 60; + const maxTime = 3600; + + const response = await assignmentDate(); + const assignmentDateValue = await response.json(); + + const answerIntervalTime = + new Date(scoreData.answerTime).getTime() - + new Date(assignmentDateValue.date).getTime(); + const normalizedTime = Math.max( + 0, + Math.min(1, (answerIntervalTime / 1000 - minTime) / (maxTime - minTime)), + ); + const point = scoreData.similarity * 70 + (1 - normalizedTime) * 30; + + const score: Score = await prisma.score.create({ + data: { + point: point, + similarity: scoreData.similarity, + imageUrl: scoreData.imageUrl, + assignmentId: scoreData.assignmentId, + userId: scoreData.userId, + }, + }); + + return score; +}; diff --git a/app/src/functions/simirality.ts b/app/src/functions/simirality.ts index 9d6a3b0..798577b 100644 --- a/app/src/functions/simirality.ts +++ b/app/src/functions/simirality.ts @@ -1,7 +1,6 @@ export const postSimilarity = async (assignment: string, words: string[]) => { // TODO クライアントサイドかサーバーサイドかでURLを変更する - // サーバーサイドの場合は、http://FastAPI:9004/similarity - const response = await fetch("http://localhost:9004/similarity", { + const response = await fetch("http://FastAPI:9004/similarity", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/app/src/types/index.tsx b/app/src/types/index.tsx index 5849032..738bd5d 100644 --- a/app/src/types/index.tsx +++ b/app/src/types/index.tsx @@ -100,13 +100,13 @@ export type todayAssignment = { assignmentId: number; english: string; assignTime?: Date; - isAnswered: boolean; + isAnswered: boolean; }; export type latestAssignment = { - assignmentId: number; - english: string; - isAnswered: boolean; + assignmentId: number; + english: string; + isAnswered: boolean; }; export type experiencePoint = { @@ -121,3 +121,18 @@ export type experiencePoint = { user?: User; }; + +export interface ScoreData { + similarity: number; + answerTime: Date; + imageUrl: string; + assignmentId: number; + userId: number; +} + +export interface ScoreResponse { + text: string; + score: number; + similarity: number; + assignmentId: number; +} From 4f55c08206e03b5542a57979c897d08f1b1fc9a7 Mon Sep 17 00:00:00 2001 From: Kubosaka Date: Fri, 15 Nov 2024 01:13:27 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF?= =?UTF-8?q?=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/app/camera/page.tsx | 86 ++----------------------------------- 1 file changed, 4 insertions(+), 82 deletions(-) diff --git a/app/src/app/camera/page.tsx b/app/src/app/camera/page.tsx index f9f630f..2603b51 100644 --- a/app/src/app/camera/page.tsx +++ b/app/src/app/camera/page.tsx @@ -16,8 +16,6 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Toaster } from "@/components/ui/sonner"; -import { shapeCaption } from "@/functions/shapeCaption"; -import { postSimilarity } from "@/functions/simirality"; import type { ScoreResponse, User, todayAssignment } from "@/types"; import type React from "react"; import { useEffect, useRef, useState } from "react"; @@ -32,21 +30,6 @@ interface ImagePreviewProps { onClick: () => void; } -interface UploadResponse { - url: string; - success: boolean; -} - -interface ScoreData { - similarity: number; - answerTime: Date; - imageUrl: string; - assignmentId: number; - userId: number; -} - -const BUCKET_NAME = "kz2404"; - const ImagePreview = ({ image, onClick }: ImagePreviewProps) => (
{ } }; - const uploadImage = async ( + const uploadImageAndRegisterScore = async ( imageData: string, ): Promise<{ data: ScoreResponse }> => { setIsUploading(true); @@ -188,8 +171,8 @@ const CameraApp = () => { // 日付取得 const date = new Date(); const thisMonth = date.getMonth() + 1; - const month = thisMonth < 10 ? "0" + thisMonth : thisMonth; - const day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate(); + const month = thisMonth < 10 ? `0${thisMonth}` : thisMonth; + const day = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate(); const formattedDate = `${date.getFullYear()}${month}${day}`; // ランダム文字列を生成する関数 @@ -216,7 +199,6 @@ const CameraApp = () => { ); const data = await response.json(); - console.log(data); return { data }; } catch (error) { @@ -225,70 +207,10 @@ const CameraApp = () => { } }; - const getCaption = async ( - imageName: string, - ): Promise<{ caption: string }> => { - try { - const response = await fetch(`/api/image?imageName=${imageName}`); - if (!response.ok) { - throw new Error("キャプションの取得に失敗しました"); - } - - return await response.json(); - } catch (error) { - console.error("キャプションの取得に失敗しました:", error); - throw error; - } - }; - - // スコア計算を行います。 - const similarityRequest = async (caption: string) => { - const words: string[] = shapeCaption(caption); - const assignmentWord: string = todayAssignment?.english || ""; - const resSimilarity = await postSimilarity(assignmentWord, words); - return { - similarity: resSimilarity.similarity as number, - assignmentId: todayAssignment?.assignmentId as number, - }; - }; - - // userIdの取得 - const getUserId = async () => { - if (!loginUser) { - console.error("ユーザー情報が取得できませんでした。"); - return; - } - const resUserId = await fetch("/api/user?uid=" + loginUser.uid, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - return await resUserId.json(); - }; - - // scoreの送信 - const submitScore = async (scoreData: ScoreData) => { - const response = await fetch("/api/score", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ scoreData }), - }); - - if (!response.ok) { - console.error("スコアの送信に失敗しました", response.statusText); - return; - } - - return await response.json(); - }; - const handleConfirm = async () => { if (tempImage) { try { - const { data } = await uploadImage(tempImage); + const { data } = await uploadImageAndRegisterScore(tempImage); setShowConfirmDialog(false); setImage(tempImage); From 0aeb05a48b8d636c43ea1b4e297c980e644120c7 Mon Sep 17 00:00:00 2001 From: Kubosaka Date: Fri, 15 Nov 2024 01:21:01 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=E3=82=B3=E3=83=B3=E3=83=95=E3=83=AA?= =?UTF-8?q?=E3=82=AF=E3=83=88=E8=A7=A3=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/app/camera/page.tsx | 328 ++++++++++++++++++------------------ 1 file changed, 163 insertions(+), 165 deletions(-) diff --git a/app/src/app/camera/page.tsx b/app/src/app/camera/page.tsx index a9d6aa3..457cfbd 100644 --- a/app/src/app/camera/page.tsx +++ b/app/src/app/camera/page.tsx @@ -1,18 +1,16 @@ "use client"; -import { Answered } from "@/components/Answered"; -import { AssignmentBadge } from "@/components/AssignmentBadge"; import { Answered } from "@/components/Answered"; import { AssignmentBadge } from "@/components/AssignmentBadge"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -23,59 +21,58 @@ import type React from "react"; import { useEffect, useRef, useState } from "react"; import { Camera, type CameraType } from "react-camera-pro"; import { toast } from "sonner"; -import { toast } from "sonner"; import AddImageIcon from "../../../public/icons/icon-add-image.svg"; import RotateCameraIcon from "../../../public/icons/icon-rotate-camera.svg"; import ShutterIcon from "../../../public/icons/icon-shutter.svg"; interface ImagePreviewProps { - image: string | null; - onClick: () => void; + image: string | null; + onClick: () => void; } const ImagePreview = ({ image, onClick }: ImagePreviewProps) => ( -
{ - if (e.key === "Enter" || e.key === " ") { - onClick(); - } - }} - /> +
{ + if (e.key === "Enter" || e.key === " ") { + onClick(); + } + }} + /> ); const DialogImagePreview = ({ image }: { image: string | null }) => { - if (!image) return null; - - return ( -
-
-
- ); + if (!image) return null; + + return ( +
+
+
+ ); }; const LoadingSpinner = () => ( -
-
-

アップロード中...

-
+
+
+

アップロード中...

+
); const imageDataToBase64 = (imageData: ImageData): string => { - const canvas = document.createElement("canvas"); - canvas.width = imageData.width; - canvas.height = imageData.height; + const canvas = document.createElement("canvas"); + canvas.width = imageData.width; + canvas.height = imageData.height; - const ctx = canvas.getContext("2d"); - if (!ctx) throw new Error("Failed to get 2D context"); + const ctx = canvas.getContext("2d"); + if (!ctx) throw new Error("Failed to get 2D context"); - ctx.putImageData(imageData, 0, 0); - return canvas.toDataURL("image/jpeg"); + ctx.putImageData(imageData, 0, 0); + return canvas.toDataURL("image/jpeg"); }; const CameraApp = () => { @@ -111,29 +108,29 @@ const CameraApp = () => { ); const assignmentData = await resAssignment.json(); - if (assignmentData.length === 0) { - setIsActive(false); - return; - } + if (assignmentData.length === 0) { + setIsActive(false); + return; + } - const isAnsweredAll = assignmentData.every( - (assignment: todayAssignment) => assignment.isAnswered - ); - if (isAnsweredAll) { - setIsActive(false); - return; - } + const isAnsweredAll = assignmentData.every( + (assignment: todayAssignment) => assignment.isAnswered, + ); + if (isAnsweredAll) { + setIsActive(false); + return; + } - const notAnsweredAssignment = assignmentData.find( - (assignment: todayAssignment) => !assignment.isAnswered - ); + const notAnsweredAssignment = assignmentData.find( + (assignment: todayAssignment) => !assignment.isAnswered, + ); - setTodayAssignment(notAnsweredAssignment); - setAssignments(assignmentData); + setTodayAssignment(notAnsweredAssignment); + setAssignments(assignmentData); - if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { - console.error("メディアデバイスAPIがサポートされていません。"); - } + if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { + console.error("メディアデバイスAPIがサポートされていません。"); + } try { const devices = await navigator.mediaDevices.enumerateDevices(); @@ -149,16 +146,16 @@ const CameraApp = () => { } }; - getDevices(); - }, []); + getDevices(); + }, []); - const switchCamera = () => { - if (devices.length > 1) { - const nextIndex = (currentDeviceIndex + 1) % devices.length; - setActiveDeviceId(devices[nextIndex].deviceId); - setCurrentDeviceIndex(nextIndex); - } - }; + const switchCamera = () => { + if (devices.length > 1) { + const nextIndex = (currentDeviceIndex + 1) % devices.length; + setActiveDeviceId(devices[nextIndex].deviceId); + setCurrentDeviceIndex(nextIndex); + } + }; const uploadImageAndRegisterScore = async ( imageData: string, @@ -168,8 +165,9 @@ const CameraApp = () => { const base64Response = await fetch(imageData); const blob = await base64Response.blob(); - // 拡張子取得 - const Extension = compressedBlob.type.split("/")[1]; + // 拡張子取得 + const compressedBlob = blob; // Assuming no compression is needed + const Extension = compressedBlob.type.split("/")[1]; // 日付取得 const date = new Date(); @@ -186,12 +184,12 @@ const CameraApp = () => { : str; }; - const randomStr = generateRandomString(); - // ファイル名作成 - const imageName = `${formattedDate}_${randomStr}.${Extension}`; + const randomStr = generateRandomString(); + // ファイル名作成 + const imageName = `${formattedDate}_${randomStr}.${Extension}`; - const formData = new FormData(); - formData.append("image", compressedBlob, imageName); + const formData = new FormData(); + formData.append("image", compressedBlob, imageName); const response = await fetch( `/api/minio?file=${imageName}&&assignment=${todayAssignment?.english}&&uid=${loginUser?.uid}&&assignmentId=${todayAssignment?.assignmentId}`, @@ -201,7 +199,7 @@ const CameraApp = () => { }, ); - const data = await response.json(); + const data = await response.json(); return { data }; } catch (error) { @@ -215,10 +213,10 @@ const CameraApp = () => { try { const { data } = await uploadImageAndRegisterScore(tempImage); - setShowConfirmDialog(false); - setImage(tempImage); - setShowImage(true); - setTempImage(null); + setShowConfirmDialog(false); + setImage(tempImage); + setShowImage(true); + setTempImage(null); const percentSimilarity = Math.floor(data.similarity * 100); @@ -234,11 +232,11 @@ const CameraApp = () => { (assignment: todayAssignment) => !assignment.isAnswered, ); - setTodayAssignment(notAnsweredAssignment); + setTodayAssignment(notAnsweredAssignment); - setIsUploading(false); - toast(message); - setAssignments(newAssignments); + setIsUploading(false); + toast(message); + setAssignments(newAssignments); if (newAssignments.every((assignment) => assignment.isAnswered)) { setIsActive(false); @@ -250,10 +248,10 @@ const CameraApp = () => { } }; - const handleCancel = () => { - setShowConfirmDialog(false); - setTempImage(null); - }; + const handleCancel = () => { + setShowConfirmDialog(false); + setTempImage(null); + }; const handleImageCapture = (capturedImage: string | ImageData) => { const imageStr = @@ -261,78 +259,78 @@ const CameraApp = () => { ? imageDataToBase64(capturedImage) : capturedImage; - setTempImage(imageStr); - setShowConfirmDialog(true); - }; - - return ( - <> - {isActive ? ( - <> -
- -
-
-
- - { - const file = event.target.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onload = () => { - handleImageCapture(reader.result as string); - }; - reader.readAsDataURL(file); - } - }} - /> -
- - -
+ setTempImage(imageStr); + setShowConfirmDialog(true); + }; + + return ( + <> + {isActive ? ( + <> +
+ +
+
+
+ + { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = () => { + handleImageCapture(reader.result as string); + }; + reader.readAsDataURL(file); + } + }} + /> +
+ + +
{ - + From 0036ac2029a5524672bc67bcee5a80027ad7e797 Mon Sep 17 00:00:00 2001 From: Kubosaka Date: Fri, 15 Nov 2024 01:24:52 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/app/api/image/route.ts | 2 +- app/src/app/api/minio/route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/app/api/image/route.ts b/app/src/app/api/image/route.ts index df461b2..e8be7ee 100644 --- a/app/src/app/api/image/route.ts +++ b/app/src/app/api/image/route.ts @@ -40,7 +40,7 @@ export async function GET( } // 画像URLからキャプションを生成する関数 -export const generateCaption = async (imageUrl: string) => { +const generateCaption = async (imageUrl: string) => { try { const completion = await openai.chat.completions.create({ model: "gpt-4o", diff --git a/app/src/app/api/minio/route.ts b/app/src/app/api/minio/route.ts index ec4ba05..573c84e 100644 --- a/app/src/app/api/minio/route.ts +++ b/app/src/app/api/minio/route.ts @@ -1,6 +1,7 @@ import * as fs from "node:fs"; import type { IncomingMessage } from "node:http"; import { Readable } from "node:stream"; +import { generateCaption } from "@/functions/gpt"; import { scoreRegister } from "@/functions/scoreRegister"; import { shapeCaption } from "@/functions/shapeCaption"; import { postSimilarity } from "@/functions/simirality"; @@ -9,7 +10,6 @@ import type { ScoreData, ScoreResponse } from "@/types"; import { formidable } from "formidable"; import { Client } from "minio"; import type { NextRequest } from "next/server"; -import { generateCaption } from "../image/route"; const minioClient = new Client({ endPoint: process.env.NEXT_PUBLIC_ENDPOINT || "", From 792f7dc491f1542a6f14bbcbbd33b5160d2e903e Mon Sep 17 00:00:00 2001 From: Kubosaka Date: Fri, 15 Nov 2024 01:35:14 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=E7=94=BB=E8=B3=AA=E8=8D=92=E3=81=8F?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/package-lock.json | 1 + app/src/app/camera/page.tsx | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index 9de9045..b4317cf 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -4990,6 +4990,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "license": "MIT", "dependencies": { "uzip": "0.20201231.0" } diff --git a/app/src/app/camera/page.tsx b/app/src/app/camera/page.tsx index 457cfbd..c43cf8f 100644 --- a/app/src/app/camera/page.tsx +++ b/app/src/app/camera/page.tsx @@ -17,6 +17,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Toaster } from "@/components/ui/sonner"; import type { ScoreResponse, User, todayAssignment } from "@/types"; +import imageCompression from "browser-image-compression"; import type React from "react"; import { useEffect, useRef, useState } from "react"; import { Camera, type CameraType } from "react-camera-pro"; @@ -163,10 +164,24 @@ const CameraApp = () => { setIsUploading(true); try { const base64Response = await fetch(imageData); - const blob = await base64Response.blob(); + const originalBlob = await base64Response.blob(); + + const compressOptions = { + maxSizeMB: 0.01, + maxWidthOrHeight: 1920, + useWebWorker: true, + }; + + const originalFile = new File([originalBlob], "tempImage", { + type: originalBlob.type, + }); + + const compressedBlob = await imageCompression( + originalFile, + compressOptions, + ); // 拡張子取得 - const compressedBlob = blob; // Assuming no compression is needed const Extension = compressedBlob.type.split("/")[1]; // 日付取得