feat: add harp branding, title autosave, and fix user lookup
This commit is contained in:
1 parent
30894e7f27
commit
25f80d4a81
26 files changed
+1258
-356
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user