feat: add harp branding, title autosave, and fix user lookup

This commit is contained in:
KevinB-T committed 2026-05-15 17:37:20 +05:30
1 parent 30894e7f27
commit 25f80d4a81
26 files changed
+1258 -356

No files matched your search

+2 -2
View File
@@ -79,7 +79,7 @@ export const env = {
cookieSameSite: stringEnv("JWT_COOKIE_SAME_SITE", "lax"),
},
upload: {
tempDir: path.resolve(backendRoot, stringEnv("UPLOAD_TEMP_DIR", ".tmp/uploads")),
tempDir: path.resolve(backendRoot, stringEnv("UPLOAD_TEMP_DIR", "/tmp/orphion/uploads")),
maxAudioSizeMb: numberEnv("MAX_AUDIO_SIZE_MB", 200),
allowedMimeTypes: stringEnv(
"AUDIO_ALLOWED_MIME_TYPES",
@@ -93,7 +93,7 @@ export const env = {
driver: stringEnv("STORAGE_DRIVER", "smb"),
basePath: path.resolve(
backendRoot,
stringEnv("STORAGE_BASE_PATH", stringEnv("REMOTE_STORAGE_PATH", "../storage/audio")),
stringEnv("STORAGE_BASE_PATH", stringEnv("REMOTE_STORAGE_PATH", "/tmp/orphion/audio")),
),
publicBaseUrl: normalizeBaseUrl(stringEnv("STORAGE_PUBLIC_BASE_URL", "")),
httpBaseUrl: normalizeBaseUrl(stringEnv("STORAGE_HTTP_BASE_URL", "")),
@@ -9,7 +9,10 @@ import {
updateTranscript,
} from "../repositories/transcriptRepository.js";
import { findUserById } from "../repositories/userRepository.js";
import { getTranscriptJob } from "../services/transcriptionService.js";
import {
getTranscriptJob,
requeueTranscriptForTranscription,
} from "../services/transcriptionService.js";
import { openAudioStream, removeAudio } from "../services/storage/storageService.js";
import { AppError } from "../utils/AppError.js";
import { sendSuccess } from "../utils/apiResponse.js";
@@ -58,7 +61,7 @@ export async function updateTranscriptText(req, res) {
}
const transcript = await updateTranscript(req.params.id, req.user.id, {
title: req.body.title ?? null,
title: Object.hasOwn(req.body, "title") ? req.body.title : undefined,
transcriptText: req.body.transcriptText,
});
sendSuccess(res, "Transcript updated", { transcript });
@@ -71,6 +74,14 @@ export async function removeTranscript(req, res) {
sendSuccess(res, "Transcript deleted");
}
export async function retranscribeTranscript(req, res) {
const result = await requeueTranscriptForTranscription({
transcriptId: req.params.id,
userId: req.user.id,
});
sendSuccess(res, "Transcript queued for re-transcription", result, 202);
}
export async function sendTranscript(req, res) {
const { transcriptId, receiverId } = req.body;
if (receiverId === Number(req.user.id)) {
@@ -184,9 +184,17 @@ export async function findTranscriptByJobForUser(jobId, userId) {
export async function updateTranscript(id, senderId, { title, transcriptText }) {
await query(
`UPDATE transcripts
SET title = :title, transcript_text = :transcriptText
SET title = CASE WHEN :hasTitle = 1 THEN :title ELSE title END,
transcript_text = CASE WHEN :hasTranscriptText = 1 THEN :transcriptText ELSE transcript_text END
WHERE id = :id AND sender_id = :senderId`,
{ id, senderId, title: title || null, transcriptText },
{
id,
senderId,
hasTitle: title === undefined ? 0 : 1,
title: title === undefined ? null : title || null,
hasTranscriptText: transcriptText === undefined ? 0 : 1,
transcriptText: transcriptText ?? null,
},
);
return findTranscriptBySenderOrId(id, senderId);
}
@@ -269,6 +277,32 @@ export async function deleteTranscript(id, senderId) {
return transcript;
}
export async function createRetranscriptionJob({ id, senderId, modelName = null }) {
return transaction(async (connection) => {
await connection.execute(
`UPDATE transcripts
SET status = 'queued', failure_reason = NULL
WHERE id = :id AND sender_id = :senderId`,
{ id, senderId },
);
await connection.execute(
`UPDATE audio_metadata
SET model_name = :modelName
WHERE transcript_id = :id`,
{ id, modelName },
);
const [job] = await connection.execute(
`INSERT INTO transcription_jobs (transcript_id, status)
VALUES (:id, 'queued')`,
{ id },
);
return job.insertId;
});
}
export async function findJobById(id) {
const rows = await query("SELECT * FROM transcription_jobs WHERE id = :id LIMIT 1", { id });
return rows[0] ?? null;
+3 -2
View File
@@ -51,6 +51,7 @@ export async function updateUserPassword(id, passwordHash) {
}
export async function listUsers({ excludeId, q = "", limit = 25 }) {
const safeLimit = Math.min(Math.max(Number(limit) || 25, 1), 50);
const rows = await query(
`SELECT id, full_name, username, email, role, created_at, updated_at
FROM users
@@ -62,8 +63,8 @@ export async function listUsers({ excludeId, q = "", limit = 25 }) {
OR email LIKE :likeQ
)
ORDER BY full_name ASC
LIMIT :limit`,
{ excludeId, q, likeQ: `%${q}%`, limit: Math.min(Number(limit) || 25, 50) },
LIMIT ${safeLimit}`,
{ excludeId, q, likeQ: `%${q}%` },
);
return rows.map(toUser);
}
@@ -5,6 +5,7 @@ import {
inbox,
listTranscripts,
removeTranscript,
retranscribeTranscript,
sendTranscript,
sent,
streamAudio,
@@ -28,6 +29,11 @@ transcriptRoutes.get("/inbox", validate(transcriptListSchema), asyncHandler(inbo
transcriptRoutes.get("/sent", validate(transcriptListSchema), asyncHandler(sent));
transcriptRoutes.post("/send", validate(sendTranscriptSchema), asyncHandler(sendTranscript));
transcriptRoutes.get("/:id/audio", validate(transcriptIdSchema), asyncHandler(streamAudio));
transcriptRoutes.post(
"/:id/retranscribe",
validate(transcriptIdSchema),
asyncHandler(retranscribeTranscript),
);
transcriptRoutes.get(
"/:id/download",
validate(transcriptIdSchema),
@@ -8,6 +8,7 @@ import {
completeTranscript,
createAudioAsset,
createQueuedTranscript,
createRetranscriptionJob,
failJob,
failTranscript,
findJobById,
@@ -119,4 +120,26 @@ export async function getTranscriptJob(transcriptId) {
return findJobByTranscriptId(transcriptId);
}
export async function requeueTranscriptForTranscription({ transcriptId, userId }) {
const transcript = await findTranscriptByIdForUser(transcriptId, userId);
if (!transcript || Number(transcript.senderId) !== Number(userId)) {
throw new AppError("Transcript not found", 404, "TRANSCRIPT_NOT_FOUND");
}
if (!transcript.audioPath) {
throw new AppError("Transcript audio asset is missing", 409, "AUDIO_MISSING");
}
const jobId = await createRetranscriptionJob({
id: transcript.id,
senderId: userId,
modelName: env.whisper.modelName,
});
enqueueTranscriptionJob(jobId);
return {
job: { id: Number(jobId), status: "queued", transcriptId: Number(transcript.id) },
transcript: await findTranscriptByIdForUser(transcript.id, userId),
};
}
export { checkWhisperHealth };
@@ -14,10 +14,14 @@ export const updateTranscriptSchema = z.object({
params: z.object({
id: z.string().regex(/^\d+$/),
}),
body: z.object({
title: z.string().trim().max(220).optional(),
transcriptText: z.string().trim().min(1),
}),
body: z
.object({
title: z.string().trim().max(220).optional(),
transcriptText: z.string().trim().min(1).optional(),
})
.refine((value) => value.title !== undefined || value.transcriptText !== undefined, {
message: "Provide a title or transcript text to update",
}),
});
export const transcriptIdSchema = z.object({