【無料公開】どうやって、1週間で翻訳サイトを公開できたのか
▷始まりは妻の一言
「読みたい論文が2つあるんだけど」
趣味でAIやノーコードツールを使ったアプリ開発をしている私に、妻が言ったセリフ。
その言葉を受けて、私が1週間で作ったWebサービスが以下の翻訳サイトです。
★作ったWebサービスは超絶シンプルな英語論文の翻訳サイト
(サーバー負荷回避のため、今はあえて使えなくしております)
画面としてはこんな感じ
翻訳したいPDFを選択すると、下に表示される
「翻訳してPDF生成」を押すと「翻訳中のローディングのマークが表示」
翻訳が完了するとPDFのプレビューとダウンロードボタンが表示される
★使ったAIサービスは、V0とChatGPT
Webサイトをできるだけ簡単に、そしておしゃれに仕上げたいと考え、私が利用した主なAIサービスとその他のウェブツールを以下に紹介します。
1. V0:フロントエンド作成に使用
フロントエンドってなんぞや?
今回のWebサイトで言うと、この部分↓
フロントエンドとは、ユーザーが直接触れる部分、つまりサイトのデザインやインターフェースを担当する部分です。V0は、このフロントエンド開発を効率化するために使用しました。初心者でも扱いやすく、見た目もスタイリッシュに仕上がる点が魅力です。(えらそうに言っていますが、私も「フロントエンド」という言葉を知ったのはつい最近です。)
2. ChatGPT:実施の翻訳やサイトの開発・コード作成に使用
論文の翻訳に大活躍!
受け取った論文のPDFを翻訳するのにChatGPTを活用しました。自然な日本語に翻訳してくれるので、専門用語が多い内容もスムーズに理解できます。また、サイトの開発・コードの作成もほぼChatGPTにやってもらっています。
その他のWebサービスとしては主に以下を使用しています。
GASは、Googleのサービスを自動化するためのスクリプト。今回は、
1. Webサイトから受け取ったPDF情報をChatGPTに送る
2. ChatGPTに翻訳するように指示
3. 受け取った翻訳テキストをPDF化
4. PDF化したテキストをプレビュー画面に表示、ダウンロードできるようにする
ここまでを実行してもらっています。
Reactは、ユーザーインターフェースを構築するためのJavaScriptライブラリです。動的でインタラクティブなWebサイトを効率的に作ることができます。(と言っていますが、Reactで使用するコードは全てV0とChatGPTに作成してもらい、エラー時の対応もChatGPTに聞いたので意味がわからなくてもできます)
GitHubは、コードのバージョン管理と共同作業を支援するプラットフォームです。Reactで作成したコードをGitHubに送り、GitHubとNetlifyを連携させることでWebサイトの公開・更新を行っています。
Netlifyは、Webサイトのデプロイメントを簡単に行えるサービスです。GitHubと連携させることで、コードをプッシュするだけで自動的にサイトが更新されます。設定もシンプルで、初心者でも手軽に使えました。
★使ったAIツール・Webサービスまとめ
以上のツールやサービスを組み合わせることで、初心者でも比較的簡単におしゃれなWebサイトを作成することができました。
(今は分からなくても大丈夫です。私自身、始めるまでReactやGitHubはどういうものか知りませんでした)
これからWebサイト制作を始める方々の参考になれば幸いです。
それでは、ここからはどのように構想し、Webサイトを作ったかを紹介したいと思います。
▷妻の要望は3つ
始めに確認したのは妻の要望。
「どんなふうに翻訳したいのか」という質問です。
「PDFでダウンロードして印刷したいんだよね」
「要約じゃなくて翻訳が欲しい。要約だと結局、全部読む羽目になる」
「ページ数は10ページぐらいのやつまで」
というわけで、妻の要望は3つ。
翻訳したテキストはPDF化し、ダウンロードできるようにする。
要約ではなく、翻訳をする。
10ページぐらいのテキスト量まで翻訳できるようにする。
▷要望を受けた私の構想
妻の要望を受けて、私が考えたWebサイトの動きとしてはこんな感じ↓
PDFを受け取る(Webサイトの画面)
・ファイルから選択
・ドラッグ&ドロップ の両方で出来たら理想翻訳ボタンをクリック(Webサイトの画面)
・「翻訳中」の表示が出ると動きが分かりやすいChatGPTにPDFの情報が送られる(GAS)
・Google Apps Script(GAS)で受けとる翻訳実行(ChatGPT 4o mini)
・安くて速いChatGPT 4o miniを使う翻訳したテキストをPDF化(GAS)
・Google Apps Scriptでやったことあるから、それを使うPDFのプレビューと、ダウンロードの表示(Webサイトの画面)
・プレビュー画面はあったほうがいい
・PDFのダウンロードボタンは必要
こんな大雑把な構想を持って、私は開発を進めました。
ここからは、実際の開発の様子と、私が辿った具体的な流れを説明したいと思います。
最後に私が今使っている、フロントエンドのコードを紹介しますので、気になる方は、ぜひ続きを見ていただければと思います!
▷V0を使ったフロントエンド作成
・V0の登録に関しては、以下の記事が詳しいのでそちらが非常に参考になります。
ここからは、私が実際にした質問の内容を紹介します。
【事前準備】
「翻訳サイト」と検索し、気に入ったサイトの画面をスクショしておく。
V0の登録が完了したら、スクショした画面を添付しつつ、以下のプロンプトを送ります。
すると、以下のように右側にフロントエンドが生成されます。
必要があれば、「もっとおしゃれにして」と指示をすると、色合いやフォントのデザイン性が増した画面が生成されます。
満足な画面ができたら、Previewの隣をクリックして、コードを表示させ、コピーしておきます。
(注意)無料プランでは1日に送れる指示に10回の限りがあるので、デザイン性の深追いはしすぎない方が良いかもしれません。
▷ChatGPTに質問、質問、質問!
ここからはChatGPTに質問しては、試し、質問しては、試しの繰り返しです。
まず、上記の方法でコピーしておいたReactのコードをChatGPTに添付し、「これを使ってウェブサイトを作りたいんだけど、Netlifyを使う手順をステップバイステップで教えて」と質問します。(以下の画像では、Googleの機能を使ってと指示しておりますが、最終的にNetlifyを使ったので初めから入れておいた方が良いと思います)
すると、アカウントの作成から手順を教えてくれます。
⚠️ここはChatGPTに質問しながら実際に自分でやってみること⚠️
なぜなら、各個人のPCのスペックや型式、種類(Windows、Mac)が違うためです。質問する時に、自分使っているPCの種類を書いてあげると正確に答えてくれる可能性が高まります。
☆重要☆
現状送ったコードではReactで動くフロントエンドのコードがあるだけですので、添付した論文を外部のサービス(今回はGoogle Apps Script)に送って処理し、処理されたものを受け取れるようにしなければいけません。
そのため、以下のような質問をし、Reactのコードの中に外部サービスに送るコードを追加してもらいます。
以下のコードとは私の前回の記事でGoogle Apps Scriptを使って数式の入ったテキストをPDF化できるようにしており、そこで使っているGoogle Apps Scriptのコードを貼り付けて、「これを参考に使って!」とChatGPTに指示をしております。
わからないことがあればChatGPTに質問していくことになりますので、
ここでは、私が実際に質問した内容を抜粋して紹介します。
例1:エラーが出た時
☞エラー内容をそのまま貼り付けて、「原因を教えて」ときく
例2:もっといい編集先を見つけたい
☞VSCodeで編集できるようにしてほしいとお願いする
例3:編集に戻れない
例4:ファイルの開き方が不明
例5:修正後のコードを途中までしか書いてくれない
例6:ChatGPTの回答の途中がわからない
こうやってひたすら質問して、回答を試していきます。
必要なのは根気です。
エラーへの対処については途中から面倒になって、エラー内容をただ貼り付けるだけになりました(笑)
質問の回数としては、今回の開発だけで100回は超えています。
最終的にローカルホストで動かせるようになれば、テストは完了。
▷実際にホスティングしてみよう
実際に動くのを確認できたら、次はWebサイトとして展開してみます。
ここでも相談相手はChatGPTです。
GitHubとはなんぞやと思いながらも、言われた通りに登録を進めていきます。
実際にホスティングサービスとしてはNetlifyを使っています。
なぜ、Netlifyなのかというと、ノーコードツールでの開発時にNetlifyを使っていて、すでにアカウントを持っているためです。
しかし、ホスティングサービスも色々あるので、ChatGPTに聞いて、好きなものを選ぶと良いと思います。
その途中でもエラーが出たら、内容を送りつけ、回答をもらう。
その繰り返しです。
そして、Deploysが完了し、作成されたWebサイト上で翻訳ができたら完成
▷出来上がりと残っている課題
実際に妻に使ってもらったところ、
翻訳したい論文が2つとも翻訳できました!
最後に・・・
私のような初心者でもこんなサービスを開発できるようになった今、
お金をかけた翻訳サービスはほぼ要らない。
これはかなり効率的なのではないでしょうか。
しかしながら、まだまだ課題はあります。例えば、
・長すぎる論文はタイムアウトになる
・プロキシサーバーの設定が未熟
・論文のフォーマットは変えず、画像がグラフを残しつつ、英文だけ翻訳することはまだできない
改善方法は今も模索中です!
今回の紹介は以上となります。
自分でも出来そうと思った方はぜひ試してみてはいかがでしょうか?
ここまでありがとうございました!
▷最後にコードの紹介
最後に、私が使っているReactのフロントエンドコードを紹介します!
これを使えば、フロントエンドは同じものが作れると思いますので、ChatGPTに質問して、自分なりに改良していって頂ければと思います。
// src/App.js
import React, { useState } from 'react';
import { Button } from "./components/ui/Button";
import { Input } from "./components/ui/Input";
import { Card } from "./components/ui/Card";
import { Upload, FileText, Download, RefreshCw, CheckCircle } from "lucide-react";
import { motion } from "framer-motion";
import * as pdfjs from 'pdfjs-dist';
const CORS_PROXY_URL = 'http://XXXXXXXXXXXXXXXXXXXXXx';
// PDF.jsのワーカースクリプトの設定
pdfjs.GlobalWorkerOptions.workerSrc = `${process.env.PUBLIC_URL}/pdf.worker.min.js`;
export default function App() {
const [file, setFile] = useState(null);
const [pdfBlobUrl, setPdfBlobUrl] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const handleFileChange = (event) => {
const selectedFile = event.target.files?.[0];
if (selectedFile) {
setFile(selectedFile);
setPdfBlobUrl("");
setError("");
}
};
const handleDrop = (event) => {
event.preventDefault();
const droppedFile = event.dataTransfer.files[0];
if (droppedFile) {
setFile(droppedFile);
setPdfBlobUrl("");
setError("");
}
};
const handleDragOver = (event) => {
event.preventDefault();
};
const extractTextFromPDF = async (file) => {
try {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise;
let text = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
const pageText = content.items.map((item) => item.str).join(' ');
text += pageText + '\n';
}
return text;
} catch (error) {
console.error('PDF processing error:', error);
throw new Error('PDFの処理中にエラーが発生しました。');
}
};
const handleTranslateAndGeneratePDF = async () => {
if (!file) {
setError("ファイルをアップロードしてください。");
return;
}
setIsLoading(true);
setError("");
setPdfBlobUrl("");
try {
let text = "";
if (file.type === "application/pdf") {
text = await extractTextFromPDF(file);
} else if (file.type === "text/plain") {
text = await file.text();
} else {
throw new Error("サポートされていないファイル形式です。");
}
if (!text.trim()) {
throw new Error("抽出されたテキストが空です。ファイルを確認してください。");
}
const data = new URLSearchParams();
data.append('text', text);
const response = await fetch(CORS_PROXY_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data.toString(),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
// Base64文字列からBlobを作成
const pdfBlob = base64ToBlob(result.pdfBase64, 'application/pdf');
const pdfBlobUrl = URL.createObjectURL(pdfBlob);
setPdfBlobUrl(pdfBlobUrl);
} else {
setError(result.error || "翻訳中にエラーが発生しました。");
}
} catch (err) {
console.error(err);
setError(err.message || "翻訳中にエラーが発生しました。");
} finally {
setIsLoading(false);
}
};
const base64ToBlob = (base64, mime) => {
const byteChars = atob(base64);
const byteNumbers = new Array(byteChars.length);
for (let i = 0; i < byteChars.length; i++) {
byteNumbers[i] = byteChars.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
return new Blob([byteArray], { type: mime });
};
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-100 via-purple-100 to-pink-100 p-4 md:p-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="flex flex-col md:flex-row gap-8">
{/* サイドバー */}
<div className="md:w-1/3 bg-gradient-to-br from-blue-500 to-indigo-600 p-8 text-white flex flex-col items-center rounded-lg shadow-lg">
<h2 className="text-3xl font-bold mb-6 text-center">英語論文の翻訳</h2>
<motion.div
className="border-2 border-dashed border-white/60 rounded-xl p-8 text-center cursor-pointer transition-all hover:border-white w-full"
onDrop={handleDrop}
onDragOver={handleDragOver}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Upload className="mx-auto mb-4 w-16 h-16 text-white" />
<p className="mb-4 text-lg">ファイルをドラッグ&ドロップ</p>
<p className="text-sm mb-4">または</p>
<Input
type="file"
onChange={handleFileChange}
className="hidden"
id="file-upload"
accept=".txt,.pdf"
/>
<label
htmlFor="file-upload"
className="bg-white text-blue-600 py-2 px-6 rounded-full cursor-pointer hover:bg-blue-100 transition-colors text-sm font-semibold whitespace-nowrap"
>
ファイルを選択
</label>
</motion.div>
{file && (
<motion.div
className="mt-6 flex items-center bg-white/10 p-3 rounded-lg w-full"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
>
<FileText className="mr-3 text-green-300" />
<span className="truncate">{file.name}</span>
</motion.div>
)}
{error && (
<motion.div
className="mt-6 text-red-300 bg-red-900/50 p-3 rounded-lg w-full"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
>
<span>{error}</span>
</motion.div>
)}
<motion.div className="w-full" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<Button
onClick={handleTranslateAndGeneratePDF}
className="mt-6 bg-green-500 hover:bg-green-600 w-full py-3 text-lg font-semibold flex items-center justify-center"
disabled={isLoading || !file}
>
{isLoading ? (
<RefreshCw className="mr-2 h-5 w-5 animate-spin" />
) : (
<CheckCircle className="mr-2 h-5 w-5" />
)}
{isLoading ? '翻訳中...' : '翻訳してPDF生成'}
</Button>
</motion.div>
</div>
{/* メインコンテンツ */}
<div className="md:w-2/3 p-8">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-semibold text-gray-800">翻訳されたPDF</h2>
{pdfBlobUrl && (
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<a
href={pdfBlobUrl}
download="translated_document.pdf"
className="bg-green-500 hover:bg-green-600 py-2 px-4 flex items-center justify-center text-white font-semibold rounded"
>
<Download className="mr-2 h-5 w-5" />
PDFをダウンロード
</a>
</motion.div>
)}
</div>
{pdfBlobUrl ? (
<div className="w-full h-full flex justify-center items-center bg-white rounded-lg shadow-lg p-4">
<iframe
src={pdfBlobUrl}
className="w-full h-full border rounded-lg"
title="PDFプレビュー"
></iframe>
</div>
) : (
<Card className="h-[calc(100vh-240px)] overflow-hidden shadow-inner bg-white rounded-xl flex items-center justify-center">
<p className="text-gray-500">ここにPDFのプレビューが表示されます。</p>
</Card>
)}
</div>
</div>
</motion.div>
</div>
);
}