ResearchGo
まもなく公開CIBES Research Centreのためのプラットフォームで、学術論文を投稿から出版まで扱います。開発は完了しており、公開前の最終確認中です。
- 担当
- システム設計と開発
- 制作年
- 2026
- 使用技術
- Next.js · Supabase (EU) · Python · FastAPI · GROBID · OpenAlex · Stripe
横にスクロールすると、図の全体を見られます。
課題
学術論文の投稿は、メールのスレッドとスプレッドシートで回っています。関わる人が2人以上になるとすぐに、版、査読者のコメント、学術誌の要件がずれていきます。編集者は「これはどの版か」「何が変わったか」「参考文献に問題はないか」「どの学術誌が合うか」という同じ質問に、毎回手作業で答えています。
どう作られているか
Supabase上のNext.jsアプリで、データはすべてEU内に保管します。著者はORCIDかメールアドレスでログインし、原稿をPDFかWordでアップロードします。原稿が届くとPythonのワーカー(FastAPI)が解析し(PDFはGROBIDで処理)、構造化したテキストを、署名とタイムスタンプ付きのコールバックで返します。このコールバックは、偽造されたジョブやリプレイされたジョブを拒否します。解析が成功するたびに新しい版として暗号化して保存しますが、それはテキストが実際に変わった場合に限ります。編集者は任意の2つの版をWord風の変更履歴で比較し、本文中の引用を参考文献リストと照合し、意味の近さで順位づけされた学術誌の候補を受け取れます。候補は、自前のワーカーで計算したEmbeddingと、OpenAlexの学術誌データをもとにしています。決済はStripeです。
重要だった判断
ルールで済むところに言語モデルは使わない
変更履歴と参考文献のチェックは決定的に動くコードで、AIは関わっていません。編集者はdiffを信頼できなければなりませんし、ルールなら毎回同じ答えを返します。
原稿は自分たちのサーバーから出さない
学術誌のマッチングでは自前のワーカーで論文をEmbeddingにするので、原稿のテキストが第三者に渡ることはありません。順位は純粋に類似度で決まります。インデックス、オープンアクセス、費用の情報はラベルとして表示するだけで、順番は変えません。
すべてのジョブに署名する
Webアプリとワーカーの間でやりとりするジョブには、HMAC署名とタイムスタンプが付いています。偽造されたジョブやリプレイされたジョブは拒否され、ワーカーがジョブの途中で落ちても、その原稿の順番は失われません。
リポジトリのコード
function diffParagraphPair(before: string, after: string): DiffSegment[] {
return diffWordsWithSpace(before, after).map((part) => ({1
op: part.added ? "ins" : part.removed ? "del" : "equal",
text: part.value,
}));
}
export function diffManuscripts(before: string, after: string): DiffResult {
const a = splitParagraphs(before);
const b = splitParagraphs(after);
// …
const parts = diffArrays(a, b);2
let i = 0;
while (i < parts.length) {
const part = parts[i];
if (!part) break;
if (!part.added && !part.removed) {
// …
i++;
continue;
}
const next = parts[i + 1];
const paired = Boolean(part.removed && next?.added);3
const removedList = part.removed ? part.value : [];
const addedList = part.added ? part.value : paired && next ? next.value : [];
const pairs = Math.min(removedList.length, addedList.length);
for (let k = 0; k < pairs; k++) {
const segments = diffParagraphPair(removedList[k] as string, addedList[k] as string);
paragraphs.push({ kind: "changed", segments });
// …
}
// …
i += paired ? 2 : 1;
}
return { paragraphs, summary };
}
2行目. 変更された段落の中では、diffを単語単位で取り、空白も保ちます。そのため、Wordの変更履歴のように読めます。
12行目. 最初のパス:段落全体に対して最長共通部分列のdiffを取り、残った部分、追加された部分、削除された部分を見つけます。
23行目. 削除のすぐ後に追加が続く場合は、別々の2つの変更ではなく、1つの段落の編集として扱います。
今の状況
開発は完了し、最終確認中です。確認が終わり次第、公開します。