Skip to content

Commit 0193cd2

Browse files
author
gauthier-th
committed
Add language to transcription
1 parent 7a2d11f commit 0193cd2

File tree

6 files changed

+103
-27
lines changed

6 files changed

+103
-27
lines changed

api/database.js

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,63 @@ const db = new sqlite3.Database(dbPath);
88

99
const saltRounds = 10;
1010

11-
// create users table if it doesn't exist with a default admin user
12-
db.serialize(() => {
13-
db.each("SELECT * FROM users", (err, rows) => {
14-
if (!err || rows) return;
15-
db.run("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password TEXT, email TEXT, role TEXt, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)", async () => {
16-
await createUser({ username: "admin", password: "admin", email: "test@mail.com", role: "admin" });
17-
console.log("Created default user: admin/admin");
11+
// create tables if they don't exist or add missing fields if needed
12+
function createTables() {
13+
const userFields = [
14+
"id INTEGER PRIMARY KEY AUTOINCREMENT",
15+
"username TEXT",
16+
"password TEXT",
17+
"email TEXT",
18+
"role TEXt",
19+
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP",
20+
];
21+
const transcriptionFields = [
22+
"id INTEGER PRIMARY KEY AUTOINCREMENT",
23+
"filename TEXT",
24+
"path TEXT",
25+
"size INTEGER",
26+
"mimetype TEXT",
27+
"duration INTEGER",
28+
"language TEXT",
29+
"status TEXT",
30+
"user_id INTEGER",
31+
"result TEXT",
32+
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP",
33+
"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP",
34+
]
35+
db.serialize(() => {
36+
db.get(`SELECT count(*) AS count FROM sqlite_master WHERE type='table' AND name='users'`, (err, rows) => {
37+
if (rows.count === 0) {
38+
db.run(`CREATE TABLE users (${userFields.join(", ")})`);
39+
createUser({ username: "admin", password: "admin", email: "test@mail.com", role: "admin" });
40+
}
41+
else {
42+
// check that all fields are present
43+
db.all(`PRAGMA table_info(users)`, (err, rows) => {
44+
const missingFields = userFields.filter(field => !rows.find(row => row.name === field.split(" ")[0]));
45+
if (missingFields.length > 0) {
46+
db.run(`ALTER TABLE users ADD COLUMN ${missingFields.join(", ")}`);
47+
}
48+
});
49+
}
50+
});
51+
db.get(`SELECT count(*) AS count FROM sqlite_master WHERE type='table' AND name='transcriptions'`, (err, rows) => {
52+
if (rows.count === 0) {
53+
db.run(`CREATE TABLE transcriptions (${transcriptionFields.join(", ")})`);
54+
}
55+
else {
56+
// check that all fields are present
57+
db.all(`PRAGMA table_info(transcriptions)`, (err, rows) => {
58+
const missingFields = transcriptionFields.filter(field => !rows.find(row => row.name === field.split(" ")[0]));
59+
if (missingFields.length > 0) {
60+
db.run(`ALTER TABLE transcriptions ADD COLUMN ${missingFields.join(", ")}`);
61+
}
62+
});
63+
}
1864
});
19-
db.run("CREATE TABLE IF NOT EXISTS transcriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT, path TEXT, size INTEGER, mimetype TEXT, duration INTEGER, status TEXT, user_id INTEGER, result TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP)");
2065
});
21-
});
66+
}
67+
createTables();
2268

2369
export async function createUser({ username, password, email, role }) {
2470
password = await bcrypt.hash(password, saltRounds);
@@ -118,9 +164,9 @@ export async function deleteUser(id) {
118164
});
119165
}
120166

121-
export async function createTranscription({ filename, path, size, mimetype, duration, status, user_id, result }) {
167+
export async function createTranscription({ filename, path, size, mimetype, duration, language, status, user_id, result }) {
122168
return new Promise((resolve, reject) => {
123-
db.run("INSERT INTO transcriptions (filename, path, size, mimetype, duration, status, user_id, result) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [filename, path, size, mimetype, duration, status, user_id, result], function (err) {
169+
db.run("INSERT INTO transcriptions (filename, path, size, mimetype, duration, language, status, user_id, result) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [filename, path, size, mimetype, duration, language, status, user_id, result], function (err) {
124170
if (err) {
125171
reject(err);
126172
} else {
@@ -166,14 +212,15 @@ export async function getTranscriptions({ filters, filterParams, limit, offset,
166212
});
167213
}
168214

169-
export async function updateTranscription({ id, filename, path, size, mimetype, duration, status, user_id, result } = {}) {
215+
export async function updateTranscription({ id, filename, path, size, mimetype, duration, language, status, user_id, result } = {}) {
170216
return new Promise(async (resolve, reject) => {
171217
const params = [];
172218
if (filename) params.push(["filename", filename]);
173219
if (path) params.push(["path", path]);
174220
if (size) params.push(["size", size]);
175221
if (mimetype) params.push(["mimetype", mimetype]);
176222
if (duration) params.push(["duration", duration]);
223+
if (language) params.push(["language", language]);
177224
if (status) params.push(["status", status]);
178225
if (user_id) params.push(["user_id", user_id]);
179226
if (result) params.push(["result", result]);

api/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ app.post('/api/transcriptions', jwtMiddleware, async (req, res) => {
240240
res.status(400).send('No file uploaded');
241241
return;
242242
}
243+
const language = req.body.language || "";
243244
const file = req.files.file;
244245
const filename = file.name;
245246
const extension = filename.split('.').pop();
@@ -253,6 +254,7 @@ app.post('/api/transcriptions', jwtMiddleware, async (req, res) => {
253254
size,
254255
mimetype,
255256
duration,
257+
language,
256258
status: 'pending',
257259
user_id: req.user.id,
258260
result: null,

api/whisper.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,23 @@ async function processTranscription(transcriptionId) {
5454
runningTranscriptions.push(transcription.id);
5555

5656
// Process the transcription
57-
const data = await whisper(path.join(dbFiles, transcription.path), {
58-
model: whisperModel,
59-
language: 'fr',
60-
output_dir: dbFiles,
61-
model_dir: whisperModelsDir,
62-
});
57+
let data;
58+
try {
59+
data = await whisper(path.join(dbFiles, transcription.path), {
60+
model: whisperModel,
61+
language: transcription.language || undefined,
62+
output_dir: dbFiles,
63+
model_dir: whisperModelsDir,
64+
});
65+
}
66+
catch (e) {
67+
console.error(e);
68+
await updateTranscription({
69+
id: transcription.id,
70+
status: 'error',
71+
});
72+
return;
73+
}
6374

6475
// Check if the transcription still exists
6576
if (!await getTranscriptionById(transcription.id)) {

app/src/components/select.jsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import { Listbox, Transition } from '@headlessui/react'
33
import { RiCheckLine } from 'react-icons/ri'
44
import { HiSelector } from 'react-icons/hi'
55

6-
export default function Select({ value, setValue, values, accessor, placeholder }) {
6+
export default function Select({ value, setValue, values, accessor, placeholder, className, orientation = "bottom" }) {
77
const containerRef = useRef(null)
88

9-
return <div>
9+
return <div className={className}>
1010
<Listbox value={value} onChange={setValue}>
1111
<div ref={containerRef} className="relative">
12-
<Listbox.Button className="relative w-full text-left bg-gray-700 input-field">
13-
<span className={`block truncate${(accessor && value ? accessor(value) : value) ? "" : " opacity-60"}`}>
12+
<Listbox.Button className="relative w-full text-left bg-transparent input-field">
13+
<span className="block truncate text-sm">
1414
{(accessor && value ? accessor(value) : value) || (placeholder ?? "Choose an option")}
1515
</span>
1616
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
@@ -27,7 +27,7 @@ export default function Select({ value, setValue, values, accessor, placeholder
2727
leaveTo="opacity-0"
2828
>
2929
{() => (
30-
<Listbox.Options className="z-10 absolute w-full py-1 mt-1 overflow-auto bg-gray-700 rounded-md shadow-lg max-h-[240px] ring-1 ring-black ring-opacity-5 focus:outline-none text-sm">
30+
<Listbox.Options className={`z-10 absolute w-full py-1 my-1${orientation === "bottom" ? "" : ""} overflow-auto bg-gray-700 rounded-md shadow-lg max-h-[240px] ring-1 ring-black ring-opacity-5 focus:outline-none text-sm`}>
3131
{values.map((value, key) => (
3232
<Listbox.Option
3333
key={key}

app/src/index.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
.file-input {
3737
/* @apply bg-transparent appearance-none border border-gray-400 rounded-lg w-full py-2 px-3 text-gray-100 leading-tight focus:outline-none focus:border-blue-400 transition-colors; */
38-
@apply block w-full text-sm text-gray-100 border border-gray-500 rounded-lg cursor-pointer bg-transparent focus:outline-none placeholder-gray-400;
38+
@apply block w-full text-sm text-gray-100 border border-gray-400 rounded-lg cursor-pointer bg-transparent focus:outline-none placeholder-gray-400;
3939
}
4040
.file-input::file-selector-button {
4141
-webkit-margin-start: -1rem;

app/src/routes/index.jsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'
33
import toast from 'react-hot-toast'
44
import { RiLoader4Fill, RiAddFill } from 'react-icons/ri'
55
import Modal from '../components/modal.jsx'
6+
import Select from '../components/select.jsx'
67

78
function formatFileSize(bytes) {
89
if (bytes < 1024) {
@@ -111,18 +112,19 @@ function App() {
111112
{transcriptions && transcriptions.length > 0 && (
112113
<div className="mt-2 w-full rounded-lg border border-gray-600 p-4">
113114
<div className="flex flex-col gap-1">
114-
<div className="grid grid-cols-8 mb-2 font-bold">
115+
<div className="grid grid-cols-9 mb-2 font-bold">
115116
<span className="col-span-3">File name</span>
116117
<span>Duration</span>
117118
<span>File size</span>
119+
<span>Language</span>
118120
{user.role === "admin" && (
119121
<span>User</span>
120122
)}
121123
<span>Status</span>
122124
<span>Actions</span>
123125
</div>
124126
{transcriptions.map((transcription) => (
125-
<div key={transcription.id} className="grid grid-cols-8 items-center">
127+
<div key={transcription.id} className="grid grid-cols-9 items-center">
126128
{transcription.filename ? (
127129
<span className="col-span-3 overflow-hidden truncate">
128130
<a
@@ -141,6 +143,7 @@ function App() {
141143
)}
142144
<span>{Math.round(transcription.duration / 60)}min{Math.round(transcription.duration % 60)}sec</span>
143145
<span>{formatFileSize(transcription.size)}</span>
146+
<span>{transcription.language || "Default"}</span>
144147
{user.role === "admin" && (
145148
<span>{transcription.username}</span>
146149
)}
@@ -164,12 +167,16 @@ function NewTranscriptionModal({ reloadList }) {
164167
const accessToken = useSelector((state) => state.accessToken)
165168
const [isNewTranscriptionOpen, setIsNewTranscriptionOpen] = useState(false)
166169
const [file, setFile] = useState(null)
170+
const [language, setLanguage] = useState(null)
171+
172+
const languages = ["Afrikaans", "Albanian", "Amharic", "Arabic", "Armenian", "Assamese", "Azerbaijani", "Bashkir", "Basque", "Belarusian", "Bengali", "Bosnian", "Breton", "Bulgarian", "Burmese", "Castilian", "Catalan", "Chinese", "Croatian", "Czech", "Danish", "Dutch", "English", "Estonian", "Faroese", "Finnish", "Flemish", "French", "Galician", "Georgian", "German", "Greek", "Gujarati", "Haitian", "Haitian Creole", "Hausa", "Hawaiian", "Hebrew", "Hindi", "Hungarian", "Icelandic", "Indonesian", "Italian", "Japanese", "Javanese", "Kannada", "Kazakh", "Khmer", "Korean", "Lao", "Latin", "Latvian", "Letzeburgesch", "Lingala", "Lithuanian", "Luxembourgish", "Macedonian", "Malagasy", "Malay", "Malayalam", "Maltese", "Maori", "Marathi", "Moldavian", "Moldovan", "Mongolian", "Myanmar", "Nepali", "Norwegian", "Nynorsk", "Occitan", "Panjabi", "Pashto", "Persian", "Polish", "Portuguese", "Punjabi", "Pushto", "Romanian", "Russian", "Sanskrit", "Serbian", "Shona", "Sindhi", "Sinhala", "Sinhalese", "Slovak", "Slovenian", "Somali", "Spanish", "Sundanese", "Swahili", "Swedish", "Tagalog", "Tajik", "Tamil", "Tatar", "Telugu", "Thai", "Tibetan", "Turkish", "Turkmen", "Ukrainian", "Urdu", "Uzbek", "Valencian", "Vietnamese", "Welsh", "Yiddish", "Yoruba"]
167173

168174
async function postTranscription() {
169175
if (!file) return
170176
try {
171177
const body = new FormData()
172178
body.append('file', file)
179+
body.append('language', language)
173180
const response = await fetch(`${import.meta.env.VITE_API_URL || "/api"}/transcriptions`, {
174181
method: 'POST',
175182
headers: {
@@ -200,13 +207,22 @@ function NewTranscriptionModal({ reloadList }) {
200207
isOpen={isNewTranscriptionOpen}
201208
onClose={() => setIsNewTranscriptionOpen(false)}
202209
title="New transcription"
210+
className="overflow-visible"
203211
>
204212
<span>Choose the file you want to transcribe:</span>
205213
<input
206214
type="file"
207-
className="mt-2 file-input"
215+
className="my-2 file-input"
208216
onChange={(e) => setFile(e.target.files[0])}
209217
/>
218+
<span>(Optionnal) Choose the language of the transcription:</span>
219+
<Select
220+
className="mt-2"
221+
value={language}
222+
setValue={setLanguage}
223+
values={languages}
224+
placeholder="Choose a language"
225+
/>
210226
<button
211227
className="button mt-4"
212228
onClick={() => postTranscription()}

0 commit comments

Comments
 (0)