├── .gitignore ├── BACKEND ├── app.py ├── main.py └── requirements.txt ├── FRONTEND ├── .gitignore ├── README.md ├── app │ ├── api │ │ ├── create │ │ │ └── route.ts │ │ ├── detail │ │ │ └── route.ts │ │ ├── list │ │ │ └── route.ts │ │ └── upload-video │ │ │ └── route.ts │ ├── create │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── list │ │ └── page.tsx │ ├── loading.tsx │ └── page.tsx ├── components │ ├── atoms │ │ ├── Logo.tsx │ │ ├── TextInput.tsx │ │ ├── VideoPlayer.tsx │ │ └── VideoUploader.tsx │ ├── molecules │ │ ├── ChatInput.tsx │ │ ├── NoVideoDialog.tsx │ │ ├── VideoCard.tsx │ │ ├── VideoDisplay.tsx │ │ ├── VideoModal.tsx │ │ ├── VideoSkeleton.tsx │ │ └── loading.tsx │ └── organisms │ │ ├── Sidebar.tsx │ │ └── VideoUpload.tsx ├── eslint.config.mjs ├── features │ ├── ChatPage.tsx │ ├── ListPage.tsx │ └── RegisterPage.tsx ├── lib │ └── db.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public │ ├── fonts │ │ ├── yu-gothic-bold.ttf │ │ ├── yu-gothic-medium.ttf │ │ ├── yu-gothic-regular.ttf │ │ └── yu-gothic-semibold.ttf │ └── imgs │ │ └── logo.png ├── tailwind.config.ts ├── tsconfig.json └── utils │ ├── convert.ts │ └── types.ts └── info.rar /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | share/python-wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .nox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *.cover 45 | *.py,cover 46 | .hypothesis/ 47 | .pytest_cache/ 48 | cover/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | db.sqlite3-journal 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | .pybuilder/ 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # IPython 75 | profile_default/ 76 | ipython_config.py 77 | 78 | .pdm.toml 79 | .pdm-python 80 | .pdm-build/ 81 | 82 | __pypackages__/ 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | 91 | # dependencies 92 | /node_modules 93 | /.pnp 94 | .pnp.js 95 | .yarn/install-state.gz 96 | 97 | # testing 98 | /coverage 99 | 100 | # next.js 101 | /.next/ 102 | /out/ 103 | 104 | # production 105 | /build 106 | 107 | # misc 108 | .DS_Store 109 | *.pem 110 | 111 | # debug 112 | npm-debug.log* 113 | yarn-debug.log* 114 | yarn-error.log* 115 | 116 | # vercel 117 | .vercel 118 | 119 | # typescript 120 | *.tsbuildinfo 121 | next-env.d.ts 122 | -------------------------------------------------------------------------------- /BACKEND/app.py: -------------------------------------------------------------------------------- 1 | from quart import Quart, request, jsonify, send_file 2 | from quart_cors import cors 3 | from sudachipy import tokenizer, dictionary 4 | import os 5 | 6 | app = Quart(__name__) 7 | cors(app) 8 | 9 | @app.route('/tokenize/tokens', methods=['POST']) 10 | async def tokenize(): 11 | tokenizer_obj = dictionary.Dictionary().create() 12 | 13 | data = await request.json 14 | text = data.get('text') 15 | tokens = tokenizer_obj.tokenize(text, tokenizer.Tokenizer.SplitMode.C) 16 | 17 | token_strs = [m.surface() for m in tokens] 18 | return token_strs 19 | 20 | if __name__ == "__main__": 21 | app.run(debug=True) -------------------------------------------------------------------------------- /BACKEND/main.py: -------------------------------------------------------------------------------- 1 | from sudachipy import tokenizer, dictionary 2 | 3 | tokenizer_obj = dictionary.Dictionary().create() 4 | text = "LINEにログインする。" 5 | tokens = tokenizer_obj.tokenize(text, tokenizer.Tokenizer.SplitMode.C) 6 | 7 | token_strs = [m.surface() for m in tokens] 8 | print(token_strs) 9 | -------------------------------------------------------------------------------- /BACKEND/requirements.txt: -------------------------------------------------------------------------------- 1 | sudachipy 2 | sudachidict_core 3 | sudachidict-full 4 | 5 | quart 6 | quart_cors -------------------------------------------------------------------------------- /FRONTEND/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /FRONTEND/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /FRONTEND/app/api/create/route.ts: -------------------------------------------------------------------------------- 1 | import { withDatabase } from "@/lib/db"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import { ResultSetHeader } from "mysql2"; 4 | 5 | export async function POST(req: NextRequest) { 6 | try { 7 | const { word, videoUrl } = await req.json(); 8 | const queryStr = `INSERT INTO mappings (word, videoUrl) VALUES (?, ?)`; 9 | 10 | const lastInsertedId = await withDatabase(async (db) => { 11 | const [result] = await db.execute(queryStr, [ 12 | word, 13 | videoUrl, 14 | ]); 15 | return result.insertId; 16 | }); 17 | 18 | return NextResponse.json({ lastInsertedId }); 19 | } catch (error) { 20 | console.error("Error in POST /api/create: ", error); 21 | return NextResponse.json( 22 | { error: "Internal Server Error" }, 23 | { status: 500 } 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /FRONTEND/app/api/detail/route.ts: -------------------------------------------------------------------------------- 1 | import { withDatabase } from "@/lib/db"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import { RowDataPacket } from "mysql2"; 4 | 5 | interface MappingResult extends RowDataPacket { 6 | videoUrl: string; 7 | } 8 | 9 | export async function POST(req: NextRequest) { 10 | try { 11 | const { token } = await req.json(); 12 | const queryStr = "SELECT videoUrl FROM mappings WHERE word = ?"; 13 | 14 | // Specify the type of the result properly 15 | const [rows] = await withDatabase(async (db) => 16 | db.query(queryStr, [token]) 17 | ); 18 | 19 | const videoUrl = rows?.[0]?.videoUrl || ""; 20 | 21 | return NextResponse.json(videoUrl); 22 | } catch (error) { 23 | console.error("Error in POST /api/detail:", error); 24 | return NextResponse.json( 25 | { error: "Internal Server Error" }, 26 | { status: 500 } 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /FRONTEND/app/api/list/route.ts: -------------------------------------------------------------------------------- 1 | import { withDatabase } from "@/lib/db"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function POST() { 5 | try { 6 | const queryStr = ` 7 | SELECT videoUrl, GROUP_CONCAT(word ORDER BY word SEPARATOR '、') AS title 8 | FROM mappings 9 | GROUP BY videoUrl 10 | `; 11 | 12 | const rows = await withDatabase(async (db) => { 13 | const [result] = await db.query(queryStr); 14 | return result; 15 | }); 16 | 17 | return NextResponse.json(rows); 18 | } catch (error) { 19 | console.error("Error in POST /api/list:", error); 20 | return NextResponse.json( 21 | { error: "Internal Server Error" }, 22 | { status: 500 } 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /FRONTEND/app/api/upload-video/route.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { writeFile } from "fs/promises"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | import { formatDateTime, getFileExtension } from "@/utils/convert"; 5 | 6 | export async function POST(req: NextRequest) { 7 | try { 8 | const video = (await req.formData()).get("video"); 9 | 10 | if (!video || !(video instanceof File)) { 11 | return NextResponse.json( 12 | { error: "No files received or incorrect type." }, 13 | { status: 400 } 14 | ); 15 | } 16 | 17 | const buffer = Buffer.from(await video.arrayBuffer()); 18 | const videoName = `${formatDateTime()}${getFileExtension(video.name)}`; 19 | const videoPath = path.join(process.cwd(), "public/videos", videoName); 20 | 21 | await writeFile(videoPath, buffer); 22 | 23 | const videoUrl = `/videos/${videoName}`; 24 | return NextResponse.json({ url: videoUrl }); 25 | } catch (error) { 26 | console.error("Error occurred while saving the file: ", error); 27 | return NextResponse.json( 28 | { error: "Failed to upload image." }, 29 | { status: 500 } 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /FRONTEND/app/create/page.tsx: -------------------------------------------------------------------------------- 1 | import RegisterPage from "@/features/RegisterPage"; 2 | 3 | 4 | export default function Page() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } -------------------------------------------------------------------------------- /FRONTEND/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KwaHash/sign-language/8b8ec208b3d375884465a17e77d754cd970cd610/FRONTEND/app/favicon.ico -------------------------------------------------------------------------------- /FRONTEND/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: "YuGothic"; 7 | src: url("/fonts/yu-gothic-regular.ttf") format("truetype"); 8 | font-weight: 400; 9 | font-style: normal; 10 | unicode-range: U+0000-002F, U+003A-0040, U+005B-0060, U+007B-3000, U+3000-30FF, 11 | U+4E00-9FFF, U+FF00-FFEF; 12 | font-display: swap; 13 | } 14 | 15 | @font-face { 16 | font-family: "YuGothic"; 17 | src: url("/fonts/yu-gothic-medium.ttf") format("truetype"); 18 | font-weight: 500; 19 | font-style: normal; 20 | unicode-range: U+0000-002F, U+003A-0040, U+005B-0060, U+007B-3000, U+3000-30FF, 21 | U+4E00-9FFF, U+FF00-FFEF; 22 | font-display: swap; 23 | } 24 | 25 | @font-face { 26 | font-family: "YuGothic"; 27 | src: url("/fonts/yu-gothic-semibold.ttf") format("truetype"); 28 | font-weight: 600; 29 | font-style: normal; 30 | unicode-range: U+0000-002F, U+003A-0040, U+005B-0060, U+007B-3000, U+3000-30FF, 31 | U+4E00-9FFF, U+FF00-FFEF; 32 | font-display: swap; 33 | } 34 | 35 | @font-face { 36 | font-family: "YuGothic"; 37 | src: url("/fonts/yu-gothic-bold.ttf") format("truetype"); 38 | font-weight: 700; 39 | font-style: normal; 40 | unicode-range: U+0000-002F, U+003A-0040, U+005B-0060, U+007B-3000, U+3000-30FF, 41 | U+4E00-9FFF, U+FF00-FFEF; 42 | font-display: swap; 43 | } 44 | 45 | :root { 46 | --background: #ffffff; 47 | --foreground: #171717; 48 | } 49 | 50 | @media (prefers-color-scheme: dark) { 51 | :root { 52 | --background: #0a0a0a; 53 | --foreground: #ededed; 54 | } 55 | } 56 | 57 | body { 58 | color: var(--foreground); 59 | background: var(--background); 60 | font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Yu Gothic", 61 | Verdana, Meiryo, "M+ 1p", sans-serif; 62 | font-weight: 500; 63 | color: #484848; 64 | } 65 | 66 | html { 67 | scroll-behavior: smooth; 68 | } 69 | -------------------------------------------------------------------------------- /FRONTEND/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import Sidebar from "@/components/organisms/Sidebar"; 3 | import "./globals.css"; 4 | 5 | export const metadata: Metadata = { 6 | title: "SignLanguage", 7 | description: "意思の疎通を図ることが難しい方を支援する手段としてITの進化に大きな期待が寄せられています。", 8 | }; 9 | 10 | export default function RootLayout({ 11 | children, 12 | }: Readonly<{ 13 | children: React.ReactNode; 14 | }>) { 15 | return ( 16 | 17 | 18 |
19 | 20 | {children} 21 |
22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /FRONTEND/app/list/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import axios from "axios"; 5 | import ListPage from "@/features/ListPage"; 6 | import type { Video } from "@/utils/types"; 7 | 8 | export default async function Page() { 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /FRONTEND/app/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Commet } from 'react-loading-indicators'; 3 | 4 | export default function Loading() { 5 | return ( 6 |
10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /FRONTEND/app/page.tsx: -------------------------------------------------------------------------------- 1 | import ChatPage from "@/features/ChatPage"; 2 | 3 | export default async function Page() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } -------------------------------------------------------------------------------- /FRONTEND/components/atoms/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from 'next/image'; 3 | 4 | const Logo = () => { 5 | return ( 6 |
7 | Logo 8 |
9 | 手話アプリ 10 |
11 |
12 | ); 13 | } 14 | 15 | export default Logo; -------------------------------------------------------------------------------- /FRONTEND/components/atoms/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type TextInputProps = { 4 | value: string; 5 | onChange: (value: string) => void; 6 | label?: string; 7 | }; 8 | 9 | export function TextInput({ value, onChange, label }: TextInputProps) { 10 | return ( 11 |
12 | {label && ( 13 | 14 | )} 15 | onChange(e.target.value)} 19 | placeholder="手話をご入力ください。" 20 | className="w-full px-4 py-2 border border-gray-300 rounded-sm focus:ring-1 focus:ring-m-blue focus:border-m-blue outline-none transition-colors duration-300 ease-out" 21 | /> 22 |
23 | ); 24 | } -------------------------------------------------------------------------------- /FRONTEND/components/atoms/VideoPlayer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IoCloseCircle } from "react-icons/io5"; 3 | 4 | type VideoPlayerProps = { 5 | videoUrl: string; 6 | fileName: string; 7 | fileSize: number; 8 | onClear: () => void; 9 | }; 10 | 11 | export function VideoPlayer({ videoUrl, fileName, fileSize, onClear }: VideoPlayerProps) { 12 | return ( 13 |
14 | 21 | 28 |
29 |

ファイル名:{fileName}

30 |

サイズ:{(fileSize / (1024 * 1024)).toFixed(2)} MB

31 |
32 |
33 | ); 34 | } -------------------------------------------------------------------------------- /FRONTEND/components/atoms/VideoUploader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FaUpload } from "react-icons/fa"; 3 | 4 | type VideoUploaderProps = { 5 | onFileChange: (e: React.ChangeEvent) => void; 6 | inputRef: React.RefObject; 7 | }; 8 | 9 | export function VideoUploader({ onFileChange, inputRef }: VideoUploaderProps) { 10 | return ( 11 | 31 | ); 32 | } -------------------------------------------------------------------------------- /FRONTEND/components/molecules/ChatInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { BsFillSendFill } from "react-icons/bs"; 3 | import { Paper, InputBase, IconButton } from '@mui/material'; 4 | 5 | type ChatInputProps = { 6 | onClick: (text: string) => void; 7 | }; 8 | 9 | export function ChatInput({ onClick }: ChatInputProps) { 10 | const [input, setInput] = useState(''); 11 | 12 | const handleSubmit = (e: React.FormEvent) => { 13 | e.preventDefault(); 14 | if (input.trim()) { 15 | onClick(input.trim()); 16 | setInput(''); 17 | } 18 | }; 19 | 20 | return ( 21 | 42 | setInput(e.target.value)} 57 | /> 58 | 78 | 79 | 80 | 81 | ); 82 | } -------------------------------------------------------------------------------- /FRONTEND/components/molecules/NoVideoDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Dialog, DialogActions, DialogTitle, useMediaQuery } from "@mui/material"; 3 | import { useTheme } from "@mui/material/styles"; 4 | 5 | interface NoVideoDialogProps { 6 | open: boolean; 7 | setOpen: (open: boolean) => void; 8 | } 9 | 10 | const NoVideoDialog: React.FC = ({ open, setOpen }) => { 11 | const theme = useTheme(); 12 | const fullScreen = useMediaQuery(theme.breakpoints.down("md")); 13 | 14 | return ( 15 | setOpen(false)} 19 | sx={{ 20 | '& .MuiDialog-paper': { 21 | minWidth: "500px", 22 | } 23 | }} 24 | > 25 | 36 | 登録されている手話がありません 37 | 38 | 42 | 58 | 59 | 60 | ) 61 | } 62 | 63 | export default NoVideoDialog; -------------------------------------------------------------------------------- /FRONTEND/components/molecules/VideoCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import type { Video } from '@/utils/types'; 4 | 5 | type VideoCardProps = { 6 | index: number; 7 | video: Video; 8 | onSelect: (video: Video) => void; 9 | }; 10 | 11 | const VideoCard: React.FC = ({ index, video, onSelect }) => { 12 | return ( 13 | onSelect(video)} 15 | className={`w-full shadow-sm hover:shadow-md transition-all duration-300 ease-out cursor-pointer h-[100px] overflow-hidden ${index % 2 ? 'bg-[#c2de8f] bg-opacity-20' : 'bg-white' 16 | }`} 17 | > 18 | 19 | {video.title} 20 | 21 | 22 |