Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions web/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# VITE_SONISORI_API_URL=https://api.sonisori.site
VITE_SONISORI_API_URL=/
VITE_SONISORI_AI_API_URL=http://127.0.0.1:5002
# VITE_SONISORI_BFF_API_URL=https://api.sonisori.site
VITE_SONISORI_BFF_API_URL=/bff

# VITE_SONISORI_AI_REST_URL=https://ai.sonisori.site
VITE_SONISORI_AI_REST_URL=/ai-rest

VITE_SONISORI_AI_SOCKET_URL=wss://ai.sonisori.site
4 changes: 2 additions & 2 deletions web/src/service/util/api.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import ky from "ky";

export const client = ky.create({
prefixUrl: import.meta.env.VITE_SONISORI_API_URL,
prefixUrl: import.meta.env.VITE_SONISORI_BFF_API_URL,
throwHttpErrors: true,
hooks: {
afterResponse: [
async (request, options, response) => {
if (response.status === 401) {
await ky
.create({ prefixUrl: import.meta.env.VITE_SONISORI_API_URL })
.create({ prefixUrl: import.meta.env.VITE_SONISORI_BFF_API_URL })
.get("api/reissue");
return ky(request, options);
}
Expand Down
12 changes: 9 additions & 3 deletions web/src/ui/component/domain/SignDetector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "@mediapipe/tasks-vision";
import { createEventListener } from "@solid-primitives/event-listener";
import { createPresence } from "@solid-primitives/presence";
import { throttle } from "@solid-primitives/scheduled";
import ky from "ky";
import { io, Socket } from "socket.io-client";
import {
Expand Down Expand Up @@ -50,7 +51,7 @@ export const createSentence = async (sign: {
}) => {
const phrase = await ky
.post(
`${import.meta.env.VITE_SONISORI_AI_API_URL}${SIGN_PHRASE_TYPE_API_ENDPOINT_MAP[sign.signPhraseType]}`,
`${import.meta.env.VITE_SONISORI_AI_REST_URL}${SIGN_PHRASE_TYPE_API_ENDPOINT_MAP[sign.signPhraseType]}`,
{
json: { prediction: sign.words },
},
Expand Down Expand Up @@ -100,6 +101,10 @@ const SignDetectorBody = (props: {
let socket: Socket;
const task = new Task();

const send = throttle((landmarks: NormalizedLandmark[][]) => {
socket.emit("predict", landmarks);
}, 200);

const streamMedia = async () => {
if (!navigator.mediaDevices?.getDisplayMedia) {
throw new Error("카메라를 사용할 수 없는 디바이스입니다.");
Expand Down Expand Up @@ -149,7 +154,7 @@ const SignDetectorBody = (props: {
const { landmarks } = handLandmarker.detectForVideo(videoRef!, time);

drawLandmarks(landmarks);
socket.emit("predict", landmarks);
send(landmarks);

if (typeof animationFrame == "number") {
animationFrame = requestAnimationFrame(predictMedia);
Expand All @@ -158,7 +163,7 @@ const SignDetectorBody = (props: {

const initialize = async () => {
try {
socket = io(import.meta.env.VITE_SONISORI_AI_API_URL, {
socket = io(import.meta.env.VITE_SONISORI_AI_SOCKET_URL, {
transports: ["websocket"],
});
socket.on("prediction_result", (data: { appended: string }) => {
Expand All @@ -185,6 +190,7 @@ const SignDetectorBody = (props: {
animationFrame = null;
}
handLandmarker.close();
send.clear();
socket.disconnect();
stream?.getTracks().forEach((track) => track.stop());
setLoaded(false);
Expand Down
98 changes: 66 additions & 32 deletions web/src/ui/screen/Dictionary.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,79 @@
import { useNavigate } from "@solidjs/router";
import { useNavigate, useParams } from "@solidjs/router";
import { createResource, For, Show } from "solid-js";

import { client } from "../../service/util/api";
import { ScrollArea } from "../component/base/ScrollAreaV2";
import { SignDetector } from "../component/domain/SignDetector";
export const Dictionary = () => {
const navigate = useNavigate();
const word = {
word: "사과",
videoUrl: "https://www.youtube.com/watch?v=9bZkp7q19f0",
};
const { id } = useParams();
const [data] = createResource(() =>
client.get(`api/words/${id}`).json<{
description: string;
resources: {
resourceType: "image" | "video";
resourceUrl: string;
}[];
word: string;
}>(),
);

return (
<div class="fixed inset-y-0 left-72 right-0">
<SignDetector onDone={() => navigate(-1)} open signPhraseType="평서문" />
<ScrollArea
class="absolute inset-0"
direction="vertical"
style={{
// sign-detector의 바닥에 붙도록
top: "calc(50vh + 70px)",
}}
>
<div class="grid flex-1 grid-cols-2 gap-7 p-7">
<div class="flex flex-col">
<p class="mb-5 text-xl font-medium text-gray-900">{word.word}</p>
{/* video placeholder */}
<video autoplay class="aspect-video" controls loop src="" />
<div class="flex-1 bg-gray-100" />
</div>
<div>
<p class="mb-3 font-medium text-gray-900">수형 사진</p>
<div class="flex gap-3">
<img alt="" class="size-32 object-contain" src="" />
<Show when={data()}>
{(data) => (
<ScrollArea
class="absolute inset-0 duration-500 animate-in fade-in slide-in-from-bottom-5"
direction="vertical"
style={{
// sign-detector의 바닥에 붙도록
top: "calc(50vh + 70px)",
}}
>
<div class="grid flex-1 grid-cols-2 gap-7 p-7">
<div class="flex flex-col">
<p class="mb-5 text-xl font-medium text-gray-900">
{data().word}
</p>
{/* video placeholder */}
<video
autoplay
class="aspect-video"
controls
loop
src={
data().resources.find(
(resource) => resource.resourceType === "video",
)?.resourceUrl
}
/>
<div class="flex-1 bg-gray-100" />
</div>
<div>
<p class="mb-3 font-medium text-gray-900">수형 사진</p>
<div class="flex gap-3">
<For
each={data().resources.filter(
(resource) => resource.resourceType === "image",
)}
>
{(resource) => (
<img
alt=""
class="size-32 object-contain"
src={resource.resourceUrl}
/>
)}
</For>
</div>
<p class="mb-3 mt-5 font-medium text-gray-900">수형 설명</p>
<p class="text-gray-800">{data().description}</p>
</div>
</div>
<p class="mb-3 mt-5 font-medium text-gray-900">수형 설명</p>
<p class="text-gray-800">
오른 손바닥으로 주먹을 쥔 왼 팔을 쓸어내린 다음, 두 주먹을 쥐고
바닥이 아래로 향하게하여 가슴 앞에서 아래로 내린다.
</p>
</div>
</div>
</ScrollArea>
</ScrollArea>
)}
</Show>
</div>
);
};
45 changes: 21 additions & 24 deletions web/src/ui/screen/Learning.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useNavigate, useParams } from "@solidjs/router";
import ky from "ky";
import {
createEffect,
createResource,
Expand All @@ -9,17 +10,16 @@ import {
} from "solid-js";
import { createStore } from "solid-js/store";

import { SignPhraseType } from "../../service/type/phrase";
import { client } from "../../service/util/api";
import { cn } from "../../service/util/cn";
import { Button } from "../component/base/Button";
import { Progress } from "../component/base/Progress";
import { SignDetector } from "../component/domain/SignDetector";

enum History {
CORRECT,
INCORRECT,
SKIP,
enum Evaluated {
CORRECT = 1,
INCORRECT = 0,
UNKNOWN = -1,
}

const ProgressStatstics = (props: {
Expand Down Expand Up @@ -82,20 +82,23 @@ const TimerStatstics = (props: {
);
};

const checkSentence = async (
sentence: string,
sign: { phraseType: SignPhraseType; words: string[] },
) => {
/**
* @todo ai 서버 마이그레이션 후 로직 수정
*/
return sentence === sign.words.join(" ") || sentence.includes(".");
const checkSentence = async (quizIndex: number, sign: { words: string[] }) => {
const phrase = await ky
.post(`${import.meta.env.VITE_SONISORI_AI_REST_URL}/evaluateMeaning`, {
json: { prediction: sign.words, quizIndex },
})
.json<Evaluated>()
.then(
(response) => response,
() => Evaluated.UNKNOWN,
);
return phrase;
};

export const Learning = () => {
const [quiz, setQuiz] = createStore({
index: 0,
history: [] as History[],
history: [] as Evaluated[],
done: false,
});

Expand All @@ -114,7 +117,7 @@ export const Learning = () => {
quiz.history.length === 0
? 100
: Math.round(
(quiz.history.filter((v) => v === History.CORRECT).length /
(quiz.history.filter((v) => v === Evaluated.CORRECT).length /
quiz.history.length) *
100,
);
Expand All @@ -129,25 +132,19 @@ export const Learning = () => {
onDone={(words) => {
if (loading()) return;
setLoading(true);
checkSentence(data()[quiz.index].sentence, {
phraseType: "평서문",
words,
})
checkSentence(data()[quiz.index].quizId, { words })
.then((result) => {
const done = quiz.index + 1 === data().length;
setQuiz((prev) => ({
index: done ? prev.index : prev.index + 1,
history: [
...prev.history,
result ? History.CORRECT : History.INCORRECT,
],
history: [...prev.history, result],
done,
}));
if (done) {
return client.post(`api/topics/${params.id}/result`, {
json: {
correctCount: quiz.history.filter(
(v) => v === History.CORRECT,
(v) => v === Evaluated.CORRECT,
).length,
},
});
Expand Down
4 changes: 2 additions & 2 deletions web/src/ui/screen/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const SignIn = () => {
<Button
as="a"
class="block w-full"
href={`${import.meta.env.VITE_SONISORI_API_URL}/oauth2/authorization/naver`}
href={`${import.meta.env.VITE_SONISORI_BFF_API_URL}/oauth2/authorization/naver`}
style={{ "background-color": "#1EC800", color: "#fff" }}
variant="secondary"
>
Expand All @@ -102,7 +102,7 @@ export const SignIn = () => {
<Button
as="a"
class="block w-full"
href={`${import.meta.env.VITE_SONISORI_API_URL}/oauth2/authorization/kakao`}
href={`${import.meta.env.VITE_SONISORI_BFF_API_URL}/oauth2/authorization/kakao`}
style={{ "background-color": "#FFEB00", color: "#000" }}
variant="secondary"
>
Expand Down
4 changes: 2 additions & 2 deletions web/src/ui/screen/SignUp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const SignUp = () => {
<Button
as="a"
class="block w-full"
href={`${import.meta.env.VITE_SONISORI_API_URL}/oauth2/authorization/naver`}
href={`${import.meta.env.VITE_SONISORI_BFF_API_URL}/oauth2/authorization/naver`}
style={{ "background-color": "#1EC800", color: "#fff" }}
variant="secondary"
>
Expand All @@ -120,7 +120,7 @@ export const SignUp = () => {
<Button
as="a"
class="block w-full"
href={`${import.meta.env.VITE_SONISORI_API_URL}/oauth2/authorization/kakao`}
href={`${import.meta.env.VITE_SONISORI_BFF_API_URL}/oauth2/authorization/kakao`}
style={{ "background-color": "#FFEB00", color: "#000" }}
variant="secondary"
>
Expand Down
5 changes: 3 additions & 2 deletions web/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

interface ImportMetaEnv {
readonly VITE_APP_TITLE: string;
readonly VITE_SONISORI_AI_API_URL: string;
readonly VITE_SONISORI_API_URL: string;
readonly VITE_SONISORI_AI_REST_URL: string;
readonly VITE_SONISORI_AI_SOCKET_URL: string;
readonly VITE_SONISORI_BFF_API_URL: string;
}

interface ImportMeta {
Expand Down
9 changes: 5 additions & 4 deletions web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ export default defineConfig({
plugins: [devtools({ autoname: true }), solid()],
server: {
proxy: {
"^/api/.*": {
"^/bff/.*": {
target: "https://api.sonisori.site",
changeOrigin: true,
cookieDomainRewrite: "localhost",
rewrite: (path) => path.replace(/^\/bff/, ""),
},
"^:5002/.*": {
target: "ws://api.sonisori.site:5002",
ws: true,
"^/ai-rest/.*": {
target: "https://ai.sonisori.site",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/ai-rest/, ""),
},
},
},
Expand Down
Loading