Compare commits

..

22 Commits

Author SHA1 Message Date
b4205049cb
merge: branch 'main' into lab/pred 2025-04-05 06:13:23 +08:00
509c10ded0
merge: branch 'feat/frontend' into feat/backend 2025-04-05 06:11:45 +08:00
99c7a34833
add: speed in benchmark result 2025-04-02 23:37:43 +08:00
d808f36c58
improve: number display in benchmark result 2025-04-02 23:32:26 +08:00
984484cc3f
improve: eliminate UI flickering between tests 2025-04-02 23:30:49 +08:00
46578db3e6
update: lower the difficulty of benchmark 2025-04-02 23:24:52 +08:00
704b5106c6
fix: difficulty too low in benchmark 2025-04-02 23:15:45 +08:00
2e702f23de
improve: UI of showing benchmark result 2025-04-02 23:13:53 +08:00
cb5e24e542
improve: UI of VDF benchmark 2025-04-02 22:55:55 +08:00
b1e071930c
fix: incorrect build config 2025-04-02 22:48:19 +08:00
c3f13cc6e3
update: VDF benchmark 2025-04-02 22:14:32 +08:00
dd829b203d
test: argon2id tester 2025-04-02 21:28:23 +08:00
41f8b42f1c
improve: the registration API 2025-04-01 10:30:44 +08:00
fba56106cc
add: user registration API 2025-03-31 20:07:58 +08:00
8d4edd43bf
update: link in about content 2025-03-31 06:39:52 +08:00
2aead46b51
update: title for frontend page 2025-03-31 06:36:36 +08:00
79af12e526
update: README 2025-03-31 06:35:44 +08:00
cfd4fc3d21
fix: incorrect API used in validation 2025-03-31 06:24:54 +08:00
7a46f31d7f
version: backend/0.2.3 2025-03-31 06:09:49 +08:00
cf33c4922d
fix: incorrect parsing for id in API route /video/:id/snapshots 2025-03-31 06:09:28 +08:00
aa75fdd63e
fix: incorrect schema check for numbers 2025-03-31 06:05:59 +08:00
9c0783c607
fix: incorrect type check for number in request params 2025-03-31 06:02:07 +08:00
16 changed files with 369 additions and 38 deletions

View File

@ -2,6 +2,11 @@
「中V档案馆」是一个旨在收录与展示「中文歌声合成作品」及有关信息的网站。
## 新闻 - 测试版本上线
目前中V档案馆上线了用于测试的前端网页和API接口它们分别位于[projectcvsa.com](https://projectcvsa.com)和[api.projectcvsa.com](https://api.projectcvsa.com)。
API调用方法请参见[接口文档](https://docs.projectcvsa.com/api-doc/)。
## 创建背景与关联工作
纵观整个互联网对于「中文歌声合成」或「中文虚拟歌手」常简称为中V或VC相关信息进行较为系统、全面地整理收集的主要有以下几个网站
@ -31,7 +36,7 @@
## 技术架构
参见[CVSA文档](https://cvsa.gitbook.io/)。
参见[CVSA文档](https://docs.projectcvsa.com/)。
## 开放许可

View File

@ -1,10 +1,12 @@
import { type Client, Pool } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import { postgresConfig } from "@core/db/pgConfig.ts";
import { postgresConfig, postgresConfigCred } from "@core/db/pgConfig.ts";
import { createMiddleware } from "hono/factory";
const pool = new Pool(postgresConfig, 4);
const poolCred = new Pool(postgresConfigCred, 2);
export const db = pool;
export const dbCred = poolCred;
export const dbMiddleware = createMiddleware(async (c, next) => {
const connection = await pool.connect();
@ -13,8 +15,16 @@ export const dbMiddleware = createMiddleware(async (c, next) => {
connection.release();
});
export const dbCredMiddleware = createMiddleware(async (c, next) => {
const connection = await poolCred.connect();
c.set("dbCred", connection);
await next();
connection.release();
})
declare module "hono" {
interface ContextVariableMap {
db: Client;
dbCred: Client;
}
}

View File

@ -1,6 +1,7 @@
{
"name": "@cvsa/backend",
"imports": {
"@rabbit-company/argon2id": "jsr:@rabbit-company/argon2id@^2.1.0",
"hono": "jsr:@hono/hono@^4.7.5",
"zod": "npm:zod",
"yup": "npm:yup"

View File

@ -1,15 +1,18 @@
import { Hono } from "hono";
import { dbMiddleware } from "./database.ts";
import { dbCredMiddleware, dbMiddleware } from "./database.ts";
import { rootHandler } from "./root.ts";
import { getSnapshotsHanlder } from "./snapshots.ts";
import { registerHandler } from "./register.ts";
export const app = new Hono();
app.use('/video/*', dbMiddleware);
app.use('/user', dbCredMiddleware);
app.get("/", ...rootHandler);
app.get('/video/:id/snapshots', ...getSnapshotsHanlder);
app.post('/user', ...registerHandler);
const fetch = app.fetch;
@ -17,4 +20,4 @@ export default {
fetch,
} satisfies Deno.ServeDefaultExport;
export const VERSION = "0.2.0";
export const VERSION = "0.3.0";

View File

@ -0,0 +1,65 @@
import { createHandlers } from "./utils.ts";
import Argon2id from "@rabbit-company/argon2id";
import { object, string, ValidationError } from "yup";
import type { Context } from "hono";
import type { Bindings, BlankEnv, BlankInput } from "hono/types";
import type { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
const RegistrationBodySchema = object({
username: string().trim().required("Username is required").max(50, "Username cannot exceed 50 characters"),
password: string().required("Password is required"),
nickname: string().optional(),
});
type ContextType = Context<BlankEnv & { Bindings: Bindings }, "/user", BlankInput>;
export const userExists = async (username: string, client: Client) => {
const query = `
SELECT * FROM users WHERE username = $1
`;
const result = await client.queryObject(query, [username]);
return result.rows.length > 0;
}
export const registerHandler = createHandlers(async (c: ContextType) => {
const client = c.get("dbCred");
try {
const body = await RegistrationBodySchema.validate(await c.req.json());
const { username, password, nickname } = body;
if (await userExists(username, client)) {
return c.json({
message: `User "${username}" already exists.`,
}, 400);
}
const hash = await Argon2id.hashEncoded(password);
const query = `
INSERT INTO users (username, password, nickname) VALUES ($1, $2, $3)
`;
await client.queryObject(query, [username, hash, nickname || null]);
return c.json({
message: `User "${username}" registered successfully.`,
}, 201);
} catch (e) {
if (e instanceof ValidationError) {
return c.json({
message: "Invalid registration data.",
errors: e.errors,
}, 400);
} else if (e instanceof SyntaxError) {
return c.json({
message: "Invalid JSON in request body.",
}, 400);
} else {
console.error("Registration error:", e);
return c.json({
message: "An unexpected error occurred during registration.",
error: (e as Error).message,
}, 500);
}
}
});

View File

@ -6,18 +6,19 @@ import type { VideoSnapshotType } from "@core/db/schema.d.ts";
import { boolean, mixed, number, object, ValidationError } from "yup";
const SnapshotQueryParamsSchema = object({
ps: number().optional().positive(),
pn: number().optional().positive(),
offset: number().optional().positive(),
ps: number().integer().optional().positive(),
pn: number().integer().optional().positive(),
offset: number().integer().optional().positive(),
reverse: boolean().optional(),
});
const idSchema = mixed().test(
"is-valid-id",
'id must be a string starting with "av" followed by digits, or "BV" followed by 10 alphanumeric characters, or a positive integer',
(value) => {
if (typeof value === "number") {
return Number.isInteger(value) && value > 0;
async (value) => {
if (value && await number().integer().isValid(value)) {
const v = parseInt(value as string);
return Number.isInteger(v) && v > 0;
}
if (typeof value === "string") {
@ -42,10 +43,13 @@ export const getSnapshotsHanlder = createHandlers(async (c: ContextType) => {
try {
const idParam = await idSchema.validate(c.req.param("id"));
let videoId: number | string = idParam as string | number;
if (typeof videoId === "string" && videoId.startsWith("av")) {
videoId = videoId.slice(2);
let videoId: string | number = idParam as string;
if (videoId.startsWith("av")) {
videoId = parseInt(videoId.slice(2));
}
else if (await number().isValid(videoId)) {
videoId = parseInt(videoId);
}
const queryParams = await SnapshotQueryParamsSchema.validate(c.req.query());
const { ps, pn, offset, reverse = false } = queryParams;

View File

@ -19,6 +19,6 @@ export default defineConfig({
allow: [".", "../../"],
},
},
plugins: [tsconfigPaths()],
plugins: [tsconfigPaths()]
},
});

View File

@ -1 +1 @@
export const VERSION = "1.2.6";
export const VERSION = "1.2.7";

View File

@ -1,23 +1,25 @@
{
"name": "frontend",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/tailwind": "^6.0.2",
"astro": "^5.5.5",
"autoprefixer": "^10.4.21",
"pg": "^8.11.11",
"postcss": "^8.5.3",
"tailwindcss": "^3.0.24",
"vite-tsconfig-paths": "^5.1.4"
},
"devDependencies": {
"@types/pg": "^8.11.11"
}
"name": "frontend",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/tailwind": "^6.0.2",
"argon2id": "^1.0.1",
"astro": "^5.5.5",
"autoprefixer": "^10.4.21",
"pg": "^8.11.11",
"postcss": "^8.5.3",
"tailwindcss": "^3.0.24",
"vite-tsconfig-paths": "^5.1.4"
},
"devDependencies": {
"@rollup/plugin-wasm": "^6.2.2",
"@types/pg": "^8.11.11"
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,219 @@
<script lang="ts">
import { N_ARRAY } from "src/const"; // 假设你的常量文件现在导出 N_ARRAY
function generateRandomBigInt(min: bigint, max: bigint) {
const range = max - min;
const bitLength = range.toString(2).length;
const byteLength = Math.ceil(bitLength / 8);
const mask = (1n << BigInt(bitLength)) - 1n; // 用于截断的掩码
let result;
do {
const randomBytes = new Uint8Array(byteLength);
crypto.getRandomValues(randomBytes);
result = 0n;
for (let i = 0; i < byteLength; i++) {
result = (result << 8n) | BigInt(randomBytes[i]);
}
result = result & mask; // 确保不超过 bitLength 位
} while (result > range);
return min + result;
}
function generateValidG(N: bigint) {
if (N <= 4n) throw new Error("N must be > 4");
while (true) {
const r = generateRandomBigInt(2n, N - 1n);
const g = (r * r) % N;
if (g !== 1n && g !== 0n && g !== N - 1n) {
return g;
}
}
}
const workerContent = `addEventListener("message", async (event) => {
const { g, N, difficulty } = event.data;
function pow(base, exponent, mod) {
let result = 1n;
base = base % mod;
while (exponent > 0n) {
if (exponent % 2n === 1n) {
result = (result * base) % mod;
}
base = (base * base) % mod;
exponent = exponent / 2n;
}
return result;
}
function computeVDFWithProgress(g, N, T, postProgress) {
let result = g;
let latestTime = performance.now();
for (let i = 0n; i < T; i++) {
result = (result * result) % N;
if (performance.now() - latestTime > 16) {
postProgress(Number(i * 100n) / Number(T));
latestTime = performance.now();
}
}
postProgress(100);
return result;
}
const startTime = performance.now();
const result = computeVDFWithProgress(g, N, difficulty, (progress) => {
postMessage({ type: "progress", N: N.toString(), difficulty: difficulty.toString(), progress });
});
const endTime = performance.now();
const timeTaken = endTime - startTime;
postMessage({ type: "result", N: N.toString(), difficulty: difficulty.toString(), time: timeTaken, result });
});
`;
let isBenchmarking = false;
interface BenchmarkResult {
N: bigint;
difficulty: bigint;
time: number;
}
let benchmarkResults: BenchmarkResult[] = [];
let currentProgress = 0;
let currentN: bigint | null = null;
let currentDifficulty: bigint | null = null;
let worker: Worker | null = null;
let currentTestIndex = 0;
const difficulties = [BigInt(20000), BigInt(200000)];
const testCombinations: { N: bigint; difficulty: bigint }[] = [];
// 创建需要测试的 N 和难度的组合
N_ARRAY.forEach((n) => {
difficulties.forEach((difficulty) => {
testCombinations.push({ N: n, difficulty });
});
});
async function startBenchmark() {
if (testCombinations.length === 0) {
alert("No N values provided in src/const N_ARRAY.");
return;
}
isBenchmarking = true;
benchmarkResults = [];
currentTestIndex = 0;
const { N, difficulty } = testCombinations[currentTestIndex];
const g = generateValidG(N);
let blob = new Blob([workerContent], { type: "text/javascript" });
worker = new Worker(window.URL.createObjectURL(blob));
worker.onmessage = (event) => {
const { type, N: resultNStr, difficulty: resultDifficultyStr, time, progress } = event.data;
const resultN = BigInt(resultNStr);
const resultDifficulty = BigInt(resultDifficultyStr);
if (type === "progress") {
currentProgress = progress;
currentN = resultN;
currentDifficulty = resultDifficulty;
} else if (type === "result") {
benchmarkResults = [...benchmarkResults, { N: resultN, difficulty: resultDifficulty, time }];
currentProgress = 0;
currentTestIndex++;
if (currentTestIndex < testCombinations.length) {
// 继续下一个测试组合
const nextTest = testCombinations[currentTestIndex];
const nextG = generateValidG(nextTest.N);
worker?.postMessage({ g: nextG, N: nextTest.N, difficulty: nextTest.difficulty });
} else {
// 所有测试完毕
isBenchmarking = false;
worker?.terminate();
worker = null;
currentN = null;
currentDifficulty = null;
}
}
};
// 开始第一个测试
worker.postMessage({ g, N, difficulty });
}
function getAccumulatedTime() {
return benchmarkResults.reduce((acc, result) => acc + result.time, 0);
}
function getAccumulatedDifficulty() {
return benchmarkResults.reduce((acc, result) => acc + Number(result.difficulty), 0);
}
function getSpeed() {
return (getAccumulatedDifficulty() / getAccumulatedTime()) * 1000;
}
</script>
<div
class="md:bg-zinc-50 md:dark:bg-zinc-800 p-6 rounded-md md:border dark:border-zinc-700 mb-6 mt-8 md:w-2/3 lg:w-1/2 xl:w-[37%] md:mx-auto"
>
<h2 class="text-xl font-bold mb-4 text-zinc-800 dark:text-zinc-200">VDF Benchmark</h2>
{#if !isBenchmarking}
<button
class="bg-blue-500 hover:bg-blue-600 duration-100 text-white font-bold py-2 px-4 rounded"
on:click={startBenchmark}
>
Start Benchmark
</button>
{/if}
{#if isBenchmarking}
<p class="mb-8 text-zinc-700 dark:text-zinc-300">
Benchmarking in progress... ({currentTestIndex + 1}/{testCombinations.length})
</p>
{#if currentN !== null && currentDifficulty !== null}
<p class="mb-2 text-zinc-700 dark:text-zinc-300">N Bits: {currentN.toString(2).length}</p>
<p class="mb-2 text-zinc-700 dark:text-zinc-300">Difficulty: {currentDifficulty}</p>
<div class="w-full bg-zinc-300 dark:bg-neutral-700 rounded-full h-1 relative overflow-hidden">
<div
class="bg-black dark:bg-white h-full rounded-full relative"
style="width: {currentProgress}%"
></div>
</div>
{/if}
{/if}
{#if benchmarkResults.length > 0 && !isBenchmarking}
<h3 class="text-lg font-bold mt-4 mb-2 text-zinc-800 dark:text-zinc-200">Benchmark Results</h3>
<p class="mb-4 text-zinc-700 dark:text-zinc-300 text-sm">
<b>Summary:</b>
{getAccumulatedDifficulty()}
calculations done in {getAccumulatedTime().toFixed(1)}ms,
speed: {getSpeed().toFixed(2)} op/s
</p>
<table class="w-full text-sm text-left rtl:text-right text-zinc-500 dark:text-zinc-400">
<thead
class="text-xs text-zinc-700 uppercase dark:text-zinc-400 border-b border-zinc-400 dark:border-zinc-500"
>
<tr>
<th scope="col" class="px-6 py-3">Time (ms)</th>
<th scope="col" class="px-6 py-3">N (bits)</th>
<th scope="col" class="px-6 py-3">T (log10)</th>
</tr>
</thead>
<tbody>
{#each benchmarkResults as result}
<tr class="border-b dark:border-zinc-700 border-zinc-200">
<td class="px-6 py-4 font-medium text-zinc-900 whitespace-nowrap dark:text-white"
>{result.time.toFixed(2)}</td
>
<td class="px-6 py-4 font-medium text-zinc-900 whitespace-nowrap dark:text-white"
>{result.N.toString(2).length}</td
>
<td class="px-6 py-4 font-medium text-zinc-900 whitespace-nowrap dark:text-white"
>{Math.log10(Number(result.difficulty)).toFixed(2)}</td
>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>

View File

@ -0,0 +1,13 @@
const N_1024 = BigInt("129023318876534346704360951712586568674758913224876821534686030409476129469193481910786173836188085930974906857867802234113909470848523288588793477904039083513378341278558405407018889387577114155572311708428733260891448259786041525189132461448841652472631435226032063278124857443496954605482776113964107326943")
const N_2048 = BigInt("23987552118069940970878653610463005981599204778388399885550631951871084945075866571231062435627294546200946516668493107358732376187241747090707087544153108117326163500579370560400058549184722138636116585329496684877258304519458316233517215780035360354808658620079068489084797380781488445517430961701007542207001544091884001098497324624368085682074645221148086075871342544591022944384890014176612259729018968864426602901247715051556212559854689574013699665035317257438297910516976812428036717668766321871780963854649899276251822244719887233041422346429752896925499321431273560130952088238625622570366815755926694833109")
const N_1792 = BigInt("23987552118069940970878653610463005981599204778388399885550631951871084945075866571231062435627294546200946516668493107358732376187241747090707087544153108117326163500579370560400058549184722138636116585329496684877258304519458316233517215780035360354808658620079068489084797380781488445517430961701007542207001544091884001098497324624368085682074645221148086075871342544591022944384890014176612259729018968864426602901247715051556212559854689574013699665035317257438297910516976812428036717668766321871780963854649899276251822244719887233041422346429752896925499321431273560130952088238625622570366815755926694833109")
const N_1536 = BigInt("1694330250214463438908848400950857073137355630337290254958754184668036770489801447652464038218330711288158361242955860326168191830448553710492926795708495297280933502917598985378231124113971732841791156356676046934277122699383776036675381503510992810963611269045078440132744168908318454891211962146563551929591147663448816841024591820348784855441153716551049843185172472891407933214238000452095646085222944171689449292644270516031799660928056315886939284985905227")
const N_3072 = BigInt("4432919939296042464443862503456460073874727648022810391370558006281079088795179408238989283371442564716849343712703672836423961818025813387453469700639513190304802553045342607888612037304066433501317127429264242784608682213025490491212489901736408833027611579294436675682774458141490718959615677971745638214649336218217578937534746160749039668886450447773018369168258067682196337978245372237157696236362344796867228581553446331915147012787367438751646936429739232247148712001806846526947508445039707404287951727838234648917450736371192435665040644040487427986702098273581288935278964444790007953559851323281510927332862225214878776790605026472021669614552481167977412450477230442015077669503312683966631454347169703030544483487968842349634064181183599641180349414682042575010303056241481622837185325228233789954078775053744988023738762706404546546146837242590884760044438874357295029411988267287001033032827035809135092270843")
const N_4096 = BigInt("703671044356805218391078271512201582198770553281951369783674142891088501340774249238173262580562112786670043634665390581120113644316651934154746357220932310140476300088580654571796404198410555061275065442553506658401183560336140989074165998202690496991174269748740565700402715364422506782445179963440819952745241176450402011121226863984008975377353558155910994380700267903933205531681076494639818328879475919332604951949178075254600102192323286738973253864238076198710173840170988339024438220034106150475640983877458155141500313471699516670799821379238743709125064098477109094533426340852518505385314780319279862586851512004686798362431227795743253799490998475141728082088984359237540124375439664236138519644100625154580910233437864328111620708697941949936338367445851449766581651338876219676721272448769082914348242483068204896479076062102236087066428603930888978596966798402915747531679758905013008059396214343112694563043918465373870648649652122703709658068801764236979191262744515840224548957285182453209028157886219424802426566456408109642062498413592155064289314088837031184200671561102160059065729282902863248815224399131391716503171191977463328439766546574118092303414702384104112719959325482439604572518549918705623086363111")
export const N_ARRAY = [N_1024, N_1536, N_1792, N_2048, N_3072, N_4096];

View File

@ -31,7 +31,8 @@
## 技术架构
参见[CVSA文档](https://cvsa.gitbook.io/)。
参见[CVSA文档](https://docs.projectcvsa.com/)。
## 开放许可

View File

@ -7,7 +7,7 @@ import "../styles/global.css";
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CVSA 前端</title>
<title>中V档案馆</title>
</head>
<body class="dark:bg-zinc-900 dark:text-zinc-100">
<slot />

View File

@ -0,0 +1,8 @@
---
import VDFtester from "@components/VDFtester.svelte";
import Layout from "@layouts/Layout.astro";
---
<Layout>
<VDFtester client:load />
</Layout>