mirror of
https://gitlab.rlp.net/proj-wise2526-video2document/video2document.git
synced 2026-06-15 18:01:52 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6083773f88 | |||
| 444d408480 | |||
| d9eacafc3a | |||
| ab737f0dc9 | |||
| 79e0c48755 | |||
| 9254ddc57f | |||
| c021272ca4 | |||
| e7e97a7f60 | |||
| 80392874bb | |||
| af794e0245 | |||
| fb173a4041 | |||
| b70680e950 | |||
| a069452f87 | |||
| fd4d342eeb | |||
| 70221683c3 | |||
| a92e33fa59 | |||
| 0dc2ebe99c | |||
| a0fe1a80e3 |
+33
-6
@@ -1,6 +1,33 @@
|
|||||||
build-job:
|
workflow:
|
||||||
script:
|
rules:
|
||||||
- echo "Building the Project.."
|
# Run the pipeline for merge requests or when committing to a branch
|
||||||
test-job:
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||||
script:
|
- if: $CI_COMMIT_BRANCH
|
||||||
- echo "Running Tests.."
|
|
||||||
|
image: python:3.14.0
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- setup
|
||||||
|
- test
|
||||||
|
|
||||||
|
setup_environment:
|
||||||
|
stage: setup
|
||||||
|
script:
|
||||||
|
- pip install --upgrade pip
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
- echo "Dependencies installed successfully."
|
||||||
|
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
- feature/ci-pipeline-s1-09a-1 # You can add more branches if needed
|
||||||
|
|
||||||
|
test_app:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- echo "Running V2D Framework basic test..."
|
||||||
|
- python -m unittest discover || echo "No tests found."
|
||||||
|
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
- feature/ci-pipeline-s1-09a-1
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health_check():
|
||||||
|
return {"status": "ok"}
|
||||||
Generated
+22
@@ -9,7 +9,9 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/axios": "^0.9.36",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"electron": "^39.1.1",
|
"electron": "^39.1.1",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"ffmpeg-static": "^5.2.0",
|
"ffmpeg-static": "^5.2.0",
|
||||||
@@ -149,6 +151,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/axios": {
|
||||||
|
"version": "0.9.36",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz",
|
||||||
|
"integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/cacheable-request": {
|
"node_modules/@types/cacheable-request": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||||
@@ -198,6 +206,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz",
|
||||||
"integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==",
|
"integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
@@ -584,6 +593,18 @@
|
|||||||
"node": ">=0.3.1"
|
"node": ">=0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "17.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||||
|
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -1824,6 +1845,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/axios": "^0.9.36",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"electron": "^39.1.1",
|
"electron": "^39.1.1",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"ffmpeg-static": "^5.2.0",
|
"ffmpeg-static": "^5.2.0",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
pytest
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
npx ts-node ./extract.ts /Users/mikehughes/Downloads/Testvideo/Kurzgesagt.mov
|
|
||||||
npx ts-node ./transcribe.ts ../storage/audio/Kurzgesagt.wav
|
|
||||||
|
|
||||||
npx ts-node ./extract.ts /Users/mikehughes/Downloads/Testvideo/GitLabMeeting.mov
|
|
||||||
npx ts-node ./transcribe.ts ../storage/audio/GitLabMeeting.wav
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/usr/bin/env ts-node
|
|
||||||
|
|
||||||
import { extractAudioFromVideo } from "../services/modules/extraction/ffmpegExtractor.ts";
|
|
||||||
|
|
||||||
const videoPath = process.argv[2];
|
|
||||||
|
|
||||||
if (!videoPath) {
|
|
||||||
console.error("Usage: ts-node extractAudio.ts <videoPath>");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
console.log(`Extracting audio from: ${videoPath}`);
|
|
||||||
|
|
||||||
await extractAudioFromVideo(videoPath); // Call the extraction function (ffmpegExtractor.ts in services/modules/extraction)
|
|
||||||
|
|
||||||
console.log("Audio extraction completed successfully.");
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Audio extraction failed:", err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { whisperLocal } from "../services/modules/transcription/local/whisperLocal.ts";
|
|
||||||
|
|
||||||
const audioPath = process.argv[2];
|
|
||||||
if (!audioPath) {
|
|
||||||
console.error("Please provide an audio file path as argument.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const whisper = new whisperLocal();
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const text = await whisper.transcribe(audioPath);
|
|
||||||
console.log(text);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Transcription failed:", err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
+1
-2
@@ -5,7 +5,7 @@ import { fileURLToPath } from "url"; // To handle __dirname in ES modules
|
|||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url); // Get current file path
|
const __filename = fileURLToPath(import.meta.url); // Get current file path
|
||||||
const __dirname = path.dirname(__filename); // Get current directory path
|
const __dirname = path.dirname(__filename); // Get current directory path
|
||||||
const transcriptsDir = path.resolve(__dirname, "../../storage/transcriptions");
|
const transcriptsDir = path.resolve(__dirname, "../../../storage/transcriptions");
|
||||||
|
|
||||||
|
|
||||||
export class whisperLocal { // is called by transcribe.ts
|
export class whisperLocal { // is called by transcribe.ts
|
||||||
@@ -26,7 +26,6 @@ export class whisperLocal { // is called by transcribe.ts
|
|||||||
|
|
||||||
async transcribe(audioPath: string): Promise<string> { //asyncronous function to transcribe audio
|
async transcribe(audioPath: string): Promise<string> { //asyncronous function to transcribe audio
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transcriptsDir = path.resolve(__dirname, "../../../../storage/transcripts"); //storage directory for transcripts
|
|
||||||
|
|
||||||
if (!fs.existsSync(transcriptsDir)) { //if transcripts directory does not exist, create it
|
if (!fs.existsSync(transcriptsDir)) { //if transcripts directory does not exist, create it
|
||||||
fs.mkdirSync(transcriptsDir, { recursive: true });
|
fs.mkdirSync(transcriptsDir, { recursive: true });
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import axios from 'axios';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const API_KEY = process.env.ASSEMBLYAI_API_KEY;
|
||||||
|
const BASE_URL = 'https://api.assemblyai.com/v2';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads audio file to AssemblyAI
|
||||||
|
*/
|
||||||
|
async function uploadAudio(audioPath: string): Promise<string> {
|
||||||
|
const audioData = fs.readFileSync(audioPath);
|
||||||
|
|
||||||
|
const response = await axios.post<{ upload_url: string }>(`${BASE_URL}/upload`, audioData, {
|
||||||
|
headers: {
|
||||||
|
'authorization': API_KEY,
|
||||||
|
'content-type': 'application/octet-stream'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.upload_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a session id (basename without extension) from a local path or a URL
|
||||||
|
*/
|
||||||
|
function getSessionId(inputPath: string): string {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(inputPath);
|
||||||
|
const base = path.basename(parsed.pathname);
|
||||||
|
return base.replace(/\.[^.]+$/, '');
|
||||||
|
} catch (err) {
|
||||||
|
// not a URL, treat as local path
|
||||||
|
return path.basename(inputPath, path.extname(inputPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates transcription job with speaker diarization
|
||||||
|
*/
|
||||||
|
async function createTranscript(audioUrl: string): Promise<string> {
|
||||||
|
const response = await axios.post<{ id: string }>(`${BASE_URL}/transcript`, {
|
||||||
|
audio_url: audioUrl,
|
||||||
|
speaker_labels: true,
|
||||||
|
language_detection: true
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'authorization': API_KEY,
|
||||||
|
'content-type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polls transcript status until completed
|
||||||
|
*/
|
||||||
|
async function pollTranscript(transcriptId: string): Promise<any> {
|
||||||
|
while (true) {
|
||||||
|
const response = await axios.get<any>(`${BASE_URL}/transcript/${transcriptId}`, {
|
||||||
|
headers: { 'authorization': API_KEY }
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = response.data.status;
|
||||||
|
|
||||||
|
if (status === 'completed') {
|
||||||
|
return response.data;
|
||||||
|
} else if (status === 'error') {
|
||||||
|
throw new Error(`Transcription failed: ${response.data.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 3 seconds before next poll
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves transcript to storage
|
||||||
|
*/
|
||||||
|
function saveTranscript(transcript: any, sessionId: string): void {
|
||||||
|
const outputDir = path.join(__dirname, '..', '..', '..', 'storage', 'transcripts');
|
||||||
|
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputPath = path.join(outputDir, `${sessionId}.json`);
|
||||||
|
fs.writeFileSync(outputPath, JSON.stringify(transcript, null, 2));
|
||||||
|
|
||||||
|
console.log(`✅ Transcript saved: ${outputPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "assembly",
|
||||||
|
type: "transcription",
|
||||||
|
displayname: "AssemblyAI",
|
||||||
|
run: async (audioPath: string) => {
|
||||||
|
try {
|
||||||
|
// Determine if audioPath is an external URL or a local file
|
||||||
|
let audioUrl: string;
|
||||||
|
if (/^https?:\/\//i.test(audioPath)) {
|
||||||
|
console.log('🔗 Using external audio URL...');
|
||||||
|
audioUrl = audioPath;
|
||||||
|
} else {
|
||||||
|
console.log('🔄 Uploading local audio...');
|
||||||
|
if (!fs.existsSync(audioPath)) {
|
||||||
|
throw new Error(`Audio file not found: ${audioPath}`);
|
||||||
|
}
|
||||||
|
audioUrl = await uploadAudio(audioPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 Creating transcript job...');
|
||||||
|
const transcriptId = await createTranscript(audioUrl);
|
||||||
|
|
||||||
|
console.log('⏳ Waiting for transcription...');
|
||||||
|
const transcript = await pollTranscript(transcriptId);
|
||||||
|
|
||||||
|
const sessionId = getSessionId(audioPath);
|
||||||
|
saveTranscript(transcript, sessionId);
|
||||||
|
|
||||||
|
return transcript;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Transcription error:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Submodule services/modules/transcription/local/whisper.cpp deleted from 999a7e0cbf
@@ -0,0 +1,52 @@
|
|||||||
|
// services/pipeline/jobs/transcribeLatest.ts
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import assembly from '../../modules/transcription/assembly';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the most recently modified .wav file in storage/audio/
|
||||||
|
*/
|
||||||
|
function getLatestWav(): string {
|
||||||
|
const audioDir = path.join(process.cwd(), 'storage', 'audio');
|
||||||
|
const files = fs.readdirSync(audioDir).filter(f => f.toLowerCase().endsWith('.wav'));
|
||||||
|
if (files.length === 0) throw new Error('⚠️ No .wav file found in storage/audio');
|
||||||
|
|
||||||
|
const newest = files
|
||||||
|
.map(f => ({ f, t: fs.statSync(path.join(audioDir, f)).mtimeMs }))
|
||||||
|
.sort((a, b) => b.t - a.t)[0].f;
|
||||||
|
|
||||||
|
return path.join(audioDir, newest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full transcription pipeline according to the defined workflow:
|
||||||
|
* 1. Audio Upload → AssemblyAI
|
||||||
|
* 2. Job Creation (transcript_id)
|
||||||
|
* 3. Polling Status (queued → processing → completed)
|
||||||
|
* 4. Download Transcript JSON
|
||||||
|
* 5. Storage: /transcripts/{session_id}.json
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
const audioPath = getLatestWav();
|
||||||
|
|
||||||
|
console.log('1️⃣ Audio Upload → AssemblyAI');
|
||||||
|
console.log(' Source:', audioPath);
|
||||||
|
|
||||||
|
console.log('2️⃣ Job Creation (transcript_id)');
|
||||||
|
console.log('3️⃣ Polling Status (queued → processing → completed)');
|
||||||
|
console.log('4️⃣ Download Transcript JSON');
|
||||||
|
console.log('5️⃣ Storage: /transcripts/{session_id}.json');
|
||||||
|
|
||||||
|
// Execute the transcription process via the AssemblyAI module
|
||||||
|
const result = await assembly.run(audioPath);
|
||||||
|
|
||||||
|
console.log('✅ Transcription completed successfully');
|
||||||
|
console.log('🆔 Transcript ID:', result.id);
|
||||||
|
console.log('📁 Transcript file saved under: storage/transcripts/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry point
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('❌ Transcription pipeline failed:', err.message || err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import assemblyModule from '../../services/modules/transcription-remote/assembly.ts';
|
||||||
|
|
||||||
|
// Test: URL passed as argument OR local file ./storage/audio/test.wav
|
||||||
|
const audioPath = process.argv[2] || './storage/audio/test.wav';
|
||||||
|
|
||||||
|
assemblyModule.run(audioPath)
|
||||||
|
.then(result => {
|
||||||
|
console.log('✅ Success!');
|
||||||
|
console.log('Transcript ID:', result.id);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('❌ Error:', error?.message || error);
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_health():
|
||||||
|
response = client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
Reference in New Issue
Block a user