feat: Add session logging, input gain, and context-aware prompts
Major features: - Session logging system with detailed segment tracking (audio files, metadata, latencies) - Input gain control (0.5x-5.0x amplifier) with soft clipping - Context-aware Whisper prompts using recent transcriptions - Comprehensive segment metadata (RMS, peak, duration, timestamps) - API latency measurements for Whisper and Claude - Audio hash-based duplicate detection - Hallucination filtering with detailed logging Changes: - Add SessionLogger class for structured session data export - Apply input gain before VAD and denoising (not just raw input) - Enhanced Pipeline with segment tracking and error logging - New UI control for input gain amplifier - Sessions saved to sessions/ directory with transcripts/ export - Improved Whisper prompt in config.json (French instructions) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9163e082da
commit
3ec2a8beca
@ -108,6 +108,7 @@ set(SOURCES_UI
|
||||
src/ui/TranslationUI.cpp
|
||||
# Utils
|
||||
src/utils/Config.cpp
|
||||
src/utils/SessionLogger.cpp
|
||||
# Core
|
||||
src/core/Pipeline.cpp
|
||||
)
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"model": "gpt-4o-mini-transcribe",
|
||||
"language": "zh",
|
||||
"temperature": 0.0,
|
||||
"prompt": "The following is a conversation in Mandarin Chinese about business, family, and daily life. Common names: Tingting, Alexis.",
|
||||
"prompt": "Transcription d'une reunion en chinois mandarin. Plusieurs interlocuteurs parlent. Ne transcris PAS: musique, silence, bruits de fond, applaudissements. Ne genere JAMAIS ces phrases: 谢谢观看, 感谢收看, 订阅, Thank you for watching, Subscribe, 再见. Si l'audio est inaudible ou juste du bruit, renvoie une chaine vide. Noms possibles: Tingting, Alexis.",
|
||||
"stream": false,
|
||||
"response_format": "text"
|
||||
},
|
||||
|
||||
BIN
secondvoice_temp.opus
Normal file
BIN
secondvoice_temp.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/001.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/001.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/002.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/002.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/003.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/003.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/004.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/004.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/005.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/005.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/006.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/006.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/007.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/007.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/008.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/008.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/009.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/009.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/010.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/010.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/011.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/011.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/012.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/012.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/013.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/013.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/014.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/014.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/015.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/015.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/016.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/016.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/017.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/017.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/018.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/018.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/019.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/019.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/020.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/020.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/021.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/021.opus
Normal file
Binary file not shown.
BIN
sessions/2025-11-24_091652/audio/022.opus
Normal file
BIN
sessions/2025-11-24_091652/audio/022.opus
Normal file
Binary file not shown.
20
sessions/2025-11-24_091652/segments/001.json
Normal file
20
sessions/2025-11-24_091652/segments/001.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 1,
|
||||
"chinese": "那是一个竹子做的。",
|
||||
"french": "C'est fait en bambou.",
|
||||
"audio": {
|
||||
"duration_seconds": 3.210,
|
||||
"rms_level": 0.0235,
|
||||
"peak_level": 0.1645,
|
||||
"filename": "001.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:17:33.048",
|
||||
"end": "2025-11-24T09:17:37.371"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 2096.9,
|
||||
"claude_latency_ms": 2187.6,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/002.json
Normal file
20
sessions/2025-11-24_091652/segments/002.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 2,
|
||||
"chinese": "那是一个竹子做的。",
|
||||
"french": "C'est fait en bambou.",
|
||||
"audio": {
|
||||
"duration_seconds": 9.620,
|
||||
"rms_level": 0.0759,
|
||||
"peak_level": 0.4828,
|
||||
"filename": "002.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:17:51.700",
|
||||
"end": "2025-11-24T09:17:54.330"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 1386.4,
|
||||
"claude_latency_ms": 1126.4,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/003.json
Normal file
20
sessions/2025-11-24_091652/segments/003.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 3,
|
||||
"chinese": "那么我们也去不花生。声音能够量就那么大。",
|
||||
"french": "Alors nous n'allons pas non plus aux cacahuètes. Le volume sonore ne peut être que si grand.",
|
||||
"audio": {
|
||||
"duration_seconds": 5.040,
|
||||
"rms_level": 0.0465,
|
||||
"peak_level": 0.2801,
|
||||
"filename": "003.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:17:56.893",
|
||||
"end": "2025-11-24T09:17:59.404"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 867.1,
|
||||
"claude_latency_ms": 1576.8,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/004.json
Normal file
20
sessions/2025-11-24_091652/segments/004.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 4,
|
||||
"chinese": "那是一个竹子做的。",
|
||||
"french": "C'est fait en bambou.",
|
||||
"audio": {
|
||||
"duration_seconds": 2.740,
|
||||
"rms_level": 0.0246,
|
||||
"peak_level": 0.1515,
|
||||
"filename": "004.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:17:59.609",
|
||||
"end": "2025-11-24T09:18:01.702"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 856.9,
|
||||
"claude_latency_ms": 1203.2,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/005.json
Normal file
20
sessions/2025-11-24_091652/segments/005.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 5,
|
||||
"chinese": "那是一个竹子做的。",
|
||||
"french": "C'est fait en bambou.",
|
||||
"audio": {
|
||||
"duration_seconds": 0.830,
|
||||
"rms_level": 0.0157,
|
||||
"peak_level": 0.1333,
|
||||
"filename": "005.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:01.752",
|
||||
"end": "2025-11-24T09:18:03.862"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 867.9,
|
||||
"claude_latency_ms": 1229.1,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/006.json
Normal file
20
sessions/2025-11-24_091652/segments/006.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 6,
|
||||
"chinese": "那么我们也去不花生。声音能够量就那么大。",
|
||||
"french": "Alors nous n'allons pas non plus aux cacahuètes. Le volume sonore peut être juste à ce niveau.",
|
||||
"audio": {
|
||||
"duration_seconds": 0.730,
|
||||
"rms_level": 0.0117,
|
||||
"peak_level": 0.1107,
|
||||
"filename": "006.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:03.529",
|
||||
"end": "2025-11-24T09:18:06.412"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 814.0,
|
||||
"claude_latency_ms": 1723.0,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/007.json
Normal file
20
sessions/2025-11-24_091652/segments/007.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 7,
|
||||
"chinese": "那是一个竹子做的。",
|
||||
"french": "C'est fait en bambou.",
|
||||
"audio": {
|
||||
"duration_seconds": 4.180,
|
||||
"rms_level": 0.0276,
|
||||
"peak_level": 0.2319,
|
||||
"filename": "007.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:09.004",
|
||||
"end": "2025-11-24T09:18:11.442"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 1173.2,
|
||||
"claude_latency_ms": 1214.7,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/008.json
Normal file
20
sessions/2025-11-24_091652/segments/008.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 8,
|
||||
"chinese": "那是一个比较古朴的。",
|
||||
"french": "C'est quelque chose de plutôt ancien et traditionnel.",
|
||||
"audio": {
|
||||
"duration_seconds": 4.410,
|
||||
"rms_level": 0.0215,
|
||||
"peak_level": 0.1613,
|
||||
"filename": "008.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:13.697",
|
||||
"end": "2025-11-24T09:18:15.990"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 1059.4,
|
||||
"claude_latency_ms": 1179.2,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/009.json
Normal file
20
sessions/2025-11-24_091652/segments/009.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 9,
|
||||
"chinese": "那么我们也去不花生。声音能够量就那么大。那是一个竹子做的。那是一个比较古朴的。",
|
||||
"french": "Alors nous n'allons pas non plus aux arachides. Le volume sonore peut être juste comme ça. C'est fait en bambou. C'est assez ancien.",
|
||||
"audio": {
|
||||
"duration_seconds": 0.840,
|
||||
"rms_level": 0.0093,
|
||||
"peak_level": 0.0592,
|
||||
"filename": "009.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:14.617",
|
||||
"end": "2025-11-24T09:18:18.713"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 1087.0,
|
||||
"claude_latency_ms": 1622.6,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/010.json
Normal file
20
sessions/2025-11-24_091652/segments/010.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 10,
|
||||
"chinese": "这些人都在干啥呢?",
|
||||
"french": "Que font ces gens ?",
|
||||
"audio": {
|
||||
"duration_seconds": 2.250,
|
||||
"rms_level": 0.0212,
|
||||
"peak_level": 0.1146,
|
||||
"filename": "010.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:19.138",
|
||||
"end": "2025-11-24T09:18:21.211"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 776.3,
|
||||
"claude_latency_ms": 1265.9,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/011.json
Normal file
20
sessions/2025-11-24_091652/segments/011.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 11,
|
||||
"chinese": "那是一个比较古朴的。",
|
||||
"french": "C'est quelque chose de plutôt ancien et traditionnel.",
|
||||
"audio": {
|
||||
"duration_seconds": 1.410,
|
||||
"rms_level": 0.0132,
|
||||
"peak_level": 0.0778,
|
||||
"filename": "011.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:20.551",
|
||||
"end": "2025-11-24T09:18:23.207"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 749.3,
|
||||
"claude_latency_ms": 1225.4,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/012.json
Normal file
20
sessions/2025-11-24_091652/segments/012.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 12,
|
||||
"chinese": "我们今天要讲的。",
|
||||
"french": "Nous allons parler aujourd'hui.",
|
||||
"audio": {
|
||||
"duration_seconds": 2.490,
|
||||
"rms_level": 0.0119,
|
||||
"peak_level": 0.0850,
|
||||
"filename": "012.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:23.302",
|
||||
"end": "2025-11-24T09:18:26.456"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 1099.0,
|
||||
"claude_latency_ms": 2022.9,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/013.json
Normal file
20
sessions/2025-11-24_091652/segments/013.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 13,
|
||||
"chinese": "这些人都在干啥呢?",
|
||||
"french": "Que font tous ces gens ?",
|
||||
"audio": {
|
||||
"duration_seconds": 1.460,
|
||||
"rms_level": 0.0124,
|
||||
"peak_level": 0.0814,
|
||||
"filename": "013.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:24.812",
|
||||
"end": "2025-11-24T09:18:29.900"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 1528.2,
|
||||
"claude_latency_ms": 1887.3,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/014.json
Normal file
20
sessions/2025-11-24_091652/segments/014.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 14,
|
||||
"chinese": "这些人都在干啥呢?",
|
||||
"french": "Qu'est-ce que ces gens sont en train de faire ?",
|
||||
"audio": {
|
||||
"duration_seconds": 2.120,
|
||||
"rms_level": 0.0138,
|
||||
"peak_level": 0.1027,
|
||||
"filename": "014.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:28.246",
|
||||
"end": "2025-11-24T09:18:32.130"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 959.4,
|
||||
"claude_latency_ms": 1242.9,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/015.json
Normal file
20
sessions/2025-11-24_091652/segments/015.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 15,
|
||||
"chinese": "假装会。",
|
||||
"french": "Faire semblant.",
|
||||
"audio": {
|
||||
"duration_seconds": 1.760,
|
||||
"rms_level": 0.0177,
|
||||
"peak_level": 0.1595,
|
||||
"filename": "015.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:30.523",
|
||||
"end": "2025-11-24T09:18:34.165"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 913.2,
|
||||
"claude_latency_ms": 1098.1,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/016.json
Normal file
20
sessions/2025-11-24_091652/segments/016.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 16,
|
||||
"chinese": "然后在这里边。",
|
||||
"french": "Ensuite à l'intérieur.",
|
||||
"audio": {
|
||||
"duration_seconds": 1.710,
|
||||
"rms_level": 0.0174,
|
||||
"peak_level": 0.1387,
|
||||
"filename": "016.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:32.295",
|
||||
"end": "2025-11-24T09:18:36.587"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 951.6,
|
||||
"claude_latency_ms": 1446.2,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/017.json
Normal file
20
sessions/2025-11-24_091652/segments/017.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 17,
|
||||
"chinese": "这些人都在干啥呢?",
|
||||
"french": "Que font ces gens ?",
|
||||
"audio": {
|
||||
"duration_seconds": 0.800,
|
||||
"rms_level": 0.0068,
|
||||
"peak_level": 0.0555,
|
||||
"filename": "017.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:33.323",
|
||||
"end": "2025-11-24T09:18:38.725"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 747.7,
|
||||
"claude_latency_ms": 1379.6,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/018.json
Normal file
20
sessions/2025-11-24_091652/segments/018.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 18,
|
||||
"chinese": "外面的样子,实际上没有。",
|
||||
"french": "L'apparence extérieure n'existe en réalité pas.",
|
||||
"audio": {
|
||||
"duration_seconds": 3.720,
|
||||
"rms_level": 0.0202,
|
||||
"peak_level": 0.1103,
|
||||
"filename": "018.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:38.208",
|
||||
"end": "2025-11-24T09:18:41.532"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 1292.5,
|
||||
"claude_latency_ms": 1465.7,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/019.json
Normal file
20
sessions/2025-11-24_091652/segments/019.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 19,
|
||||
"chinese": "然后在这里边。",
|
||||
"french": "Ensuite à l'intérieur.",
|
||||
"audio": {
|
||||
"duration_seconds": 0.830,
|
||||
"rms_level": 0.0085,
|
||||
"peak_level": 0.0587,
|
||||
"filename": "019.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:39.724",
|
||||
"end": "2025-11-24T09:18:43.611"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 914.8,
|
||||
"claude_latency_ms": 1152.8,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/020.json
Normal file
20
sessions/2025-11-24_091652/segments/020.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 20,
|
||||
"chinese": "哪些外面的情况比较烂。",
|
||||
"french": "Quelles sont les situations extérieures qui sont plutôt mauvaises.",
|
||||
"audio": {
|
||||
"duration_seconds": 2.940,
|
||||
"rms_level": 0.0150,
|
||||
"peak_level": 0.0884,
|
||||
"filename": "020.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:42.920",
|
||||
"end": "2025-11-24T09:18:45.934"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 966.4,
|
||||
"claude_latency_ms": 1321.6,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/021.json
Normal file
20
sessions/2025-11-24_091652/segments/021.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 21,
|
||||
"chinese": "然后来看一下。",
|
||||
"french": "Ensuite, viens voir.",
|
||||
"audio": {
|
||||
"duration_seconds": 2.110,
|
||||
"rms_level": 0.0259,
|
||||
"peak_level": 0.1535,
|
||||
"filename": "021.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:45.067",
|
||||
"end": "2025-11-24T09:18:47.886"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 794.9,
|
||||
"claude_latency_ms": 1126.7,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
20
sessions/2025-11-24_091652/segments/022.json
Normal file
20
sessions/2025-11-24_091652/segments/022.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 22,
|
||||
"chinese": "然后来看一下。",
|
||||
"french": "Ensuite, viens jeter un coup d'œil.",
|
||||
"audio": {
|
||||
"duration_seconds": 3.430,
|
||||
"rms_level": 0.0162,
|
||||
"peak_level": 0.1952,
|
||||
"filename": "022.opus"
|
||||
},
|
||||
"timestamps": {
|
||||
"start": "2025-11-24T09:18:48.616",
|
||||
"end": "2025-11-24T09:18:51.376"
|
||||
},
|
||||
"processing": {
|
||||
"whisper_latency_ms": 1079.8,
|
||||
"claude_latency_ms": 1639.0,
|
||||
"was_filtered": false
|
||||
}
|
||||
}
|
||||
@ -43,18 +43,32 @@ int AudioCapture::audioCallback(const void* input, void* output,
|
||||
const float* in = static_cast<const float*>(input);
|
||||
unsigned long sample_count = frame_count * self->channels_;
|
||||
|
||||
// === APPLY INPUT GAIN ===
|
||||
// Get gain value and apply to input samples
|
||||
float gain = self->input_gain_.load(std::memory_order_relaxed);
|
||||
std::vector<float> amplified_samples(sample_count);
|
||||
for (unsigned long i = 0; i < sample_count; ++i) {
|
||||
// Apply gain with soft clipping to avoid harsh distortion
|
||||
float sample = in[i] * gain;
|
||||
// Soft clip to [-1, 1] range using tanh-like curve
|
||||
if (sample > 1.0f) sample = 1.0f - 1.0f / (1.0f + sample - 1.0f);
|
||||
else if (sample < -1.0f) sample = -1.0f + 1.0f / (1.0f - sample - 1.0f);
|
||||
amplified_samples[i] = sample;
|
||||
}
|
||||
const float* amplified_in = amplified_samples.data();
|
||||
|
||||
// === REAL-TIME DENOISING ===
|
||||
// Process audio through RNNoise in real-time for meter display
|
||||
std::vector<float> denoised_samples;
|
||||
if (self->noise_reducer_ && self->noise_reducer_->isEnabled()) {
|
||||
denoised_samples = self->noise_reducer_->processRealtime(in, sample_count);
|
||||
denoised_samples = self->noise_reducer_->processRealtime(amplified_in, sample_count);
|
||||
}
|
||||
|
||||
// Calculate RMS and Peak from RAW audio (for VAD detection)
|
||||
// Calculate RMS and Peak from AMPLIFIED audio (for VAD detection)
|
||||
float raw_sum_squared = 0.0f;
|
||||
float raw_max_amp = 0.0f;
|
||||
for (unsigned long i = 0; i < sample_count; ++i) {
|
||||
float sample = in[i];
|
||||
float sample = amplified_in[i];
|
||||
raw_sum_squared += sample * sample;
|
||||
if (std::abs(sample) > raw_max_amp) {
|
||||
raw_max_amp = std::abs(sample);
|
||||
@ -158,9 +172,9 @@ int AudioCapture::audioCallback(const void* input, void* output,
|
||||
self->speech_buffer_.insert(self->speech_buffer_.end(),
|
||||
denoised_samples.begin(), denoised_samples.end());
|
||||
} else {
|
||||
// Fallback to raw if denoising disabled
|
||||
// Fallback to amplified if denoising disabled
|
||||
for (unsigned long i = 0; i < sample_count; ++i) {
|
||||
self->speech_buffer_.push_back(in[i]);
|
||||
self->speech_buffer_.push_back(amplified_in[i]);
|
||||
}
|
||||
}
|
||||
self->speech_samples_count_ += sample_count;
|
||||
@ -194,13 +208,13 @@ int AudioCapture::audioCallback(const void* input, void* output,
|
||||
|
||||
// If we were speaking and now have enough silence, flush
|
||||
if (self->speech_buffer_.size() > 0) {
|
||||
// Add trailing silence (denoised)
|
||||
// Add trailing silence (denoised or amplified)
|
||||
if (!denoised_samples.empty()) {
|
||||
self->speech_buffer_.insert(self->speech_buffer_.end(),
|
||||
denoised_samples.begin(), denoised_samples.end());
|
||||
} else {
|
||||
for (unsigned long i = 0; i < sample_count; ++i) {
|
||||
self->speech_buffer_.push_back(in[i]);
|
||||
self->speech_buffer_.push_back(amplified_in[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -33,6 +33,10 @@ public:
|
||||
vad_rms_threshold_ = rms_threshold;
|
||||
vad_peak_threshold_ = peak_threshold;
|
||||
}
|
||||
|
||||
// Input gain (amplifier) - can be adjusted in real-time from UI
|
||||
void setInputGain(float gain) { input_gain_ = gain; }
|
||||
float getInputGain() const { return input_gain_; }
|
||||
void setSilenceDuration(int ms) { silence_duration_ms_ = ms; }
|
||||
void setMinSpeechDuration(int ms) { min_speech_duration_ms_ = ms; }
|
||||
void setMaxSpeechDuration(int ms) { max_speech_duration_ms_ = ms; }
|
||||
@ -90,6 +94,9 @@ private:
|
||||
std::atomic<float> current_rms_{0.0f};
|
||||
std::atomic<float> current_peak_{0.0f};
|
||||
|
||||
// Input gain (amplifier) - 1.0 = no change, >1.0 = amplify, <1.0 = attenuate
|
||||
std::atomic<float> input_gain_{1.0f};
|
||||
|
||||
// Noise reduction
|
||||
std::unique_ptr<NoiseReducer> noise_reducer_;
|
||||
};
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include "../api/ClaudeClient.h"
|
||||
#include "../ui/TranslationUI.h"
|
||||
#include "../utils/Config.h"
|
||||
#include "../utils/SessionLogger.h"
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
@ -61,6 +62,10 @@ bool Pipeline::initialize() {
|
||||
// Create recordings directory if it doesn't exist
|
||||
std::filesystem::create_directories(config.getRecordingConfig().output_directory);
|
||||
|
||||
// Initialize session logger
|
||||
session_logger_ = std::make_unique<SessionLogger>();
|
||||
session_logger_->setModels(config.getWhisperConfig().model, config.getClaudeConfig().model);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -71,6 +76,15 @@ bool Pipeline::start() {
|
||||
|
||||
running_ = true;
|
||||
|
||||
// Start session logging
|
||||
if (session_logger_) {
|
||||
session_logger_->startSession();
|
||||
session_logger_->setVadSettings(
|
||||
ui_ ? ui_->getVadThreshold() : 0.02f,
|
||||
ui_ ? ui_->getVadPeakThreshold() : 0.08f
|
||||
);
|
||||
}
|
||||
|
||||
// Start background threads
|
||||
audio_thread_ = std::thread(&Pipeline::audioThread, this);
|
||||
processing_thread_ = std::thread(&Pipeline::processingThread, this);
|
||||
@ -126,6 +140,11 @@ void Pipeline::stop() {
|
||||
transcript_ss << "transcripts/transcript_" << timestamp.str() << ".txt";
|
||||
ui_->exportTranscript(transcript_ss.str());
|
||||
}
|
||||
|
||||
// End session logging
|
||||
if (session_logger_) {
|
||||
session_logger_->endSession();
|
||||
}
|
||||
}
|
||||
|
||||
void Pipeline::audioThread() {
|
||||
@ -137,11 +156,25 @@ void Pipeline::audioThread() {
|
||||
// Add to full recording
|
||||
full_recording_->addSamples(audio_data);
|
||||
|
||||
// Push to processing queue
|
||||
// Calculate RMS and peak levels for metadata
|
||||
float sum_squared = 0.0f;
|
||||
float max_amp = 0.0f;
|
||||
for (const float& sample : audio_data) {
|
||||
sum_squared += sample * sample;
|
||||
if (std::abs(sample) > max_amp) {
|
||||
max_amp = std::abs(sample);
|
||||
}
|
||||
}
|
||||
float rms = audio_data.empty() ? 0.0f : std::sqrt(sum_squared / audio_data.size());
|
||||
|
||||
// Push to processing queue with metadata
|
||||
AudioChunk chunk;
|
||||
chunk.data = audio_data;
|
||||
chunk.sample_rate = config.getAudioConfig().sample_rate;
|
||||
chunk.channels = config.getAudioConfig().channels;
|
||||
chunk.rms_level = rms;
|
||||
chunk.peak_level = max_amp;
|
||||
chunk.timestamp = std::chrono::system_clock::now();
|
||||
|
||||
audio_queue_.push(std::move(chunk));
|
||||
});
|
||||
@ -170,7 +203,36 @@ void Pipeline::processingThread() {
|
||||
float duration = static_cast<float>(chunk.data.size()) / (chunk.sample_rate * chunk.channels);
|
||||
std::cout << "[Processing] Speech segment: " << duration << "s" << std::endl;
|
||||
|
||||
// Transcribe with Whisper
|
||||
// Prepare segment data for logging
|
||||
SegmentData segment;
|
||||
segment.id = session_logger_ ? session_logger_->getNextSegmentId() : 0;
|
||||
segment.start_time = chunk.timestamp;
|
||||
segment.duration_seconds = duration;
|
||||
segment.rms_level = chunk.rms_level;
|
||||
segment.peak_level = chunk.peak_level;
|
||||
segment.was_filtered = false;
|
||||
|
||||
// Save audio for this segment (also calculates hashes for duplicate detection)
|
||||
if (session_logger_) {
|
||||
segment.audio_filename = session_logger_->saveSegmentAudio(
|
||||
segment.id, chunk.data, chunk.sample_rate, chunk.channels,
|
||||
segment.audio_hashes);
|
||||
}
|
||||
|
||||
// Build dynamic prompt with recent context
|
||||
std::string whisper_prompt = config.getWhisperConfig().prompt;
|
||||
if (session_logger_) {
|
||||
auto recent = session_logger_->getRecentTranscriptions(3);
|
||||
if (!recent.empty()) {
|
||||
whisper_prompt += "\n\nRecent context: ";
|
||||
for (const auto& t : recent) {
|
||||
whisper_prompt += t + " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transcribe with Whisper (measure latency)
|
||||
auto whisper_start = std::chrono::steady_clock::now();
|
||||
auto whisper_result = whisper_client_->transcribe(
|
||||
chunk.data,
|
||||
chunk.sample_rate,
|
||||
@ -178,12 +240,18 @@ void Pipeline::processingThread() {
|
||||
config.getWhisperConfig().model,
|
||||
config.getWhisperConfig().language,
|
||||
config.getWhisperConfig().temperature,
|
||||
config.getWhisperConfig().prompt,
|
||||
whisper_prompt,
|
||||
config.getWhisperConfig().response_format
|
||||
);
|
||||
auto whisper_end = std::chrono::steady_clock::now();
|
||||
segment.whisper_latency_ms = std::chrono::duration<float, std::milli>(whisper_end - whisper_start).count();
|
||||
|
||||
if (!whisper_result.has_value()) {
|
||||
std::cerr << "Whisper transcription failed" << std::endl;
|
||||
segment.was_filtered = true;
|
||||
segment.filter_reason = "whisper_api_error";
|
||||
segment.end_time = std::chrono::system_clock::now();
|
||||
if (session_logger_) session_logger_->logSegment(segment);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -195,6 +263,10 @@ void Pipeline::processingThread() {
|
||||
size_t end = text.find_last_not_of(" \t\n\r");
|
||||
if (start == std::string::npos) {
|
||||
std::cout << "[Skip] Empty transcription" << std::endl;
|
||||
segment.was_filtered = true;
|
||||
segment.filter_reason = "empty";
|
||||
segment.end_time = std::chrono::system_clock::now();
|
||||
if (session_logger_) session_logger_->logSegment(segment);
|
||||
continue;
|
||||
}
|
||||
text = text.substr(start, end - start + 1);
|
||||
@ -267,6 +339,11 @@ void Pipeline::processingThread() {
|
||||
|
||||
if (is_garbage) {
|
||||
std::cout << "[Skip] Filtered: " << text << std::endl;
|
||||
segment.chinese = text;
|
||||
segment.was_filtered = true;
|
||||
segment.filter_reason = "hallucination";
|
||||
segment.end_time = std::chrono::system_clock::now();
|
||||
if (session_logger_) session_logger_->logSegment(segment);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -275,16 +352,24 @@ void Pipeline::processingThread() {
|
||||
ui_->addAudioCost(duration);
|
||||
}
|
||||
|
||||
// Translate with Claude
|
||||
// Translate with Claude (measure latency)
|
||||
auto claude_start = std::chrono::steady_clock::now();
|
||||
auto claude_result = claude_client_->translate(
|
||||
text,
|
||||
config.getClaudeConfig().system_prompt,
|
||||
config.getClaudeConfig().max_tokens,
|
||||
config.getClaudeConfig().temperature
|
||||
);
|
||||
auto claude_end = std::chrono::steady_clock::now();
|
||||
segment.claude_latency_ms = std::chrono::duration<float, std::milli>(claude_end - claude_start).count();
|
||||
|
||||
if (!claude_result.has_value()) {
|
||||
std::cerr << "Claude translation failed" << std::endl;
|
||||
segment.chinese = text;
|
||||
segment.was_filtered = true;
|
||||
segment.filter_reason = "claude_api_error";
|
||||
segment.end_time = std::chrono::system_clock::now();
|
||||
if (session_logger_) session_logger_->logSegment(segment);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -293,6 +378,14 @@ void Pipeline::processingThread() {
|
||||
ui_->addClaudeCost();
|
||||
}
|
||||
|
||||
// Log successful segment
|
||||
segment.chinese = text;
|
||||
segment.french = claude_result->text;
|
||||
segment.end_time = std::chrono::system_clock::now();
|
||||
if (session_logger_) {
|
||||
session_logger_->logSegment(segment);
|
||||
}
|
||||
|
||||
// Simple accumulation
|
||||
if (!accumulated_chinese_.empty()) {
|
||||
accumulated_chinese_ += " ";
|
||||
@ -317,12 +410,13 @@ void Pipeline::processingThread() {
|
||||
void Pipeline::update() {
|
||||
if (!ui_) return;
|
||||
|
||||
// Sync VAD thresholds from UI to AudioCapture
|
||||
// Sync VAD thresholds and input gain from UI to AudioCapture
|
||||
if (audio_capture_) {
|
||||
audio_capture_->setVadThresholds(
|
||||
ui_->getVadThreshold(),
|
||||
ui_->getVadPeakThreshold()
|
||||
);
|
||||
audio_capture_->setInputGain(ui_->getInputGain());
|
||||
|
||||
// Update UI with audio levels
|
||||
ui_->setCurrentRMS(audio_capture_->getCurrentRMS());
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include "../utils/ThreadSafeQueue.h"
|
||||
|
||||
namespace secondvoice {
|
||||
@ -14,11 +15,15 @@ class WhisperClient;
|
||||
class ClaudeClient;
|
||||
class TranslationUI;
|
||||
class AudioBuffer;
|
||||
class SessionLogger;
|
||||
|
||||
struct AudioChunk {
|
||||
std::vector<float> data;
|
||||
int sample_rate;
|
||||
int channels;
|
||||
float rms_level;
|
||||
float peak_level;
|
||||
std::chrono::system_clock::time_point timestamp;
|
||||
};
|
||||
|
||||
class Pipeline {
|
||||
@ -48,6 +53,7 @@ private:
|
||||
std::unique_ptr<ClaudeClient> claude_client_;
|
||||
std::unique_ptr<TranslationUI> ui_;
|
||||
std::unique_ptr<AudioBuffer> full_recording_;
|
||||
std::unique_ptr<SessionLogger> session_logger_;
|
||||
|
||||
ThreadSafeQueue<AudioChunk> audio_queue_;
|
||||
|
||||
|
||||
@ -400,6 +400,17 @@ void TranslationUI::renderAudioPanel() {
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Input Gain (Amplifier)
|
||||
ImGui::Text("Input Gain");
|
||||
ImGui::SliderFloat("##input_gain", &input_gain_, 0.5f, 5.0f, "x%.1f");
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Amplify microphone input (1.0 = normal)");
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// VAD Threshold sliders
|
||||
ImGui::Text("VAD Settings");
|
||||
ImGui::Spacing();
|
||||
|
||||
@ -44,6 +44,9 @@ public:
|
||||
float getVadThreshold() const { return vad_threshold_; }
|
||||
float getVadPeakThreshold() const { return vad_peak_threshold_; }
|
||||
|
||||
// Input gain (amplifier)
|
||||
float getInputGain() const { return input_gain_; }
|
||||
|
||||
private:
|
||||
void renderAccumulated();
|
||||
void renderTranslations();
|
||||
@ -71,6 +74,7 @@ private:
|
||||
float current_peak_ = 0.0f;
|
||||
float vad_threshold_ = 0.02f; // 2x higher to avoid false triggers
|
||||
float vad_peak_threshold_ = 0.08f; // 2x higher
|
||||
float input_gain_ = 1.0f; // Input amplifier (1.0 = no change)
|
||||
|
||||
// Cost tracking
|
||||
float total_audio_seconds_ = 0.0f;
|
||||
|
||||
412
src/utils/SessionLogger.cpp
Normal file
412
src/utils/SessionLogger.cpp
Normal file
@ -0,0 +1,412 @@
|
||||
#include "SessionLogger.h"
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <cstring>
|
||||
|
||||
// For Opus encoding (FetchContent paths)
|
||||
#include <opus.h>
|
||||
#include <ogg/ogg.h>
|
||||
|
||||
namespace {
|
||||
// Simple FNV-1a hash for audio fingerprinting
|
||||
uint64_t fnv1a_hash(const float* data, size_t count) {
|
||||
const uint64_t FNV_PRIME = 0x100000001b3ULL;
|
||||
const uint64_t FNV_OFFSET = 0xcbf29ce484222325ULL;
|
||||
|
||||
uint64_t hash = FNV_OFFSET;
|
||||
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(data);
|
||||
size_t byte_count = count * sizeof(float);
|
||||
|
||||
for (size_t i = 0; i < byte_count; ++i) {
|
||||
hash ^= bytes[i];
|
||||
hash *= FNV_PRIME;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::string hash_to_hex(uint64_t hash) {
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::setfill('0') << std::setw(16) << hash;
|
||||
return ss.str();
|
||||
}
|
||||
}
|
||||
|
||||
namespace secondvoice {
|
||||
|
||||
SessionLogger::SessionLogger() {
|
||||
metadata_ = {};
|
||||
std::srand(static_cast<unsigned int>(std::time(nullptr))); // For OGG stream IDs
|
||||
}
|
||||
|
||||
SessionLogger::~SessionLogger() {
|
||||
if (session_active_) {
|
||||
endSession();
|
||||
}
|
||||
}
|
||||
|
||||
bool SessionLogger::startSession() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
// Generate session ID from timestamp
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto time_t = std::chrono::system_clock::to_time_t(now);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H%M%S");
|
||||
metadata_.session_id = ss.str();
|
||||
metadata_.start_time = now;
|
||||
|
||||
// Create directory structure: sessions/YYYY-MM-DD_HHMMSS/
|
||||
session_path_ = "sessions/" + metadata_.session_id;
|
||||
segments_path_ = session_path_ + "/segments";
|
||||
|
||||
try {
|
||||
std::filesystem::create_directories(segments_path_);
|
||||
std::filesystem::create_directories(session_path_ + "/audio");
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[SessionLogger] Failed to create directories: " << e.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize metadata
|
||||
metadata_.total_segments = 0;
|
||||
metadata_.filtered_segments = 0;
|
||||
metadata_.total_audio_seconds = 0.0f;
|
||||
metadata_.total_cost_estimate = 0.0f;
|
||||
|
||||
next_segment_id_ = 1;
|
||||
segments_.clear();
|
||||
session_active_ = true;
|
||||
|
||||
std::cout << "[SessionLogger] Started session: " << metadata_.session_id << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SessionLogger::endSession() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (!session_active_) return;
|
||||
|
||||
metadata_.end_time = std::chrono::system_clock::now();
|
||||
metadata_.total_segments = static_cast<int>(segments_.size());
|
||||
|
||||
writeSessionJson();
|
||||
|
||||
session_active_ = false;
|
||||
std::cout << "[SessionLogger] Ended session: " << metadata_.session_id
|
||||
<< " (" << metadata_.total_segments << " segments)" << std::endl;
|
||||
}
|
||||
|
||||
void SessionLogger::logSegment(const SegmentData& segment) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (!session_active_) return;
|
||||
|
||||
segments_.push_back(segment);
|
||||
next_segment_id_++; // Increment for next segment
|
||||
|
||||
if (segment.was_filtered) {
|
||||
metadata_.filtered_segments++;
|
||||
}
|
||||
metadata_.total_audio_seconds += segment.duration_seconds;
|
||||
|
||||
// Estimate cost: Whisper $0.006/min, Claude ~$0.001/call
|
||||
float whisper_cost = (segment.duration_seconds / 60.0f) * 0.006f;
|
||||
float claude_cost = segment.was_filtered ? 0.0f : 0.001f;
|
||||
metadata_.total_cost_estimate += whisper_cost + claude_cost;
|
||||
|
||||
writeSegmentJson(segment);
|
||||
}
|
||||
|
||||
std::string SessionLogger::saveSegmentAudio(int segment_id, const std::vector<float>& audio_data,
|
||||
int sample_rate, int channels,
|
||||
std::vector<std::string>& out_hashes) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
out_hashes.clear();
|
||||
|
||||
if (!session_active_) return "";
|
||||
|
||||
// Calculate hash per second of audio
|
||||
size_t samples_per_second = sample_rate * channels;
|
||||
size_t num_seconds = (audio_data.size() + samples_per_second - 1) / samples_per_second;
|
||||
|
||||
for (size_t sec = 0; sec < num_seconds; ++sec) {
|
||||
size_t start = sec * samples_per_second;
|
||||
size_t end = std::min(start + samples_per_second, audio_data.size());
|
||||
size_t count = end - start;
|
||||
|
||||
uint64_t hash = fnv1a_hash(audio_data.data() + start, count);
|
||||
out_hashes.push_back(hash_to_hex(hash));
|
||||
}
|
||||
|
||||
std::cout << "[SessionLogger] Audio hashes (" << num_seconds << "s): ";
|
||||
for (const auto& h : out_hashes) {
|
||||
std::cout << h.substr(0, 8) << " "; // Print first 8 chars for brevity
|
||||
}
|
||||
std::cout << std::endl;
|
||||
|
||||
// Format: audio/001.opus
|
||||
std::stringstream filename_ss;
|
||||
filename_ss << std::setfill('0') << std::setw(3) << segment_id << ".opus";
|
||||
std::string filename = filename_ss.str();
|
||||
std::string filepath = session_path_ + "/audio/" + filename;
|
||||
|
||||
// Encode to Opus/OGG
|
||||
int error;
|
||||
OpusEncoder* encoder = opus_encoder_create(sample_rate, channels, OPUS_APPLICATION_VOIP, &error);
|
||||
if (error != OPUS_OK || !encoder) {
|
||||
std::cerr << "[SessionLogger] Failed to create Opus encoder: " << opus_strerror(error) << std::endl;
|
||||
return "";
|
||||
}
|
||||
|
||||
// Set bitrate to 24kbps for speech
|
||||
opus_encoder_ctl(encoder, OPUS_SET_BITRATE(24000));
|
||||
|
||||
// Open output file
|
||||
std::ofstream outfile(filepath, std::ios::binary);
|
||||
if (!outfile.is_open()) {
|
||||
opus_encoder_destroy(encoder);
|
||||
std::cerr << "[SessionLogger] Failed to open file: " << filepath << std::endl;
|
||||
return "";
|
||||
}
|
||||
|
||||
// OGG stream setup
|
||||
ogg_stream_state os;
|
||||
ogg_stream_init(&os, rand());
|
||||
|
||||
// Write Opus header
|
||||
unsigned char header[19];
|
||||
memcpy(header, "OpusHead", 8);
|
||||
header[8] = 1; // version
|
||||
header[9] = channels;
|
||||
header[10] = 0; header[11] = 0; // pre-skip (little endian)
|
||||
uint32_t rate = sample_rate;
|
||||
memcpy(&header[12], &rate, 4); // sample rate (little endian)
|
||||
header[16] = 0; header[17] = 0; // output gain
|
||||
header[18] = 0; // channel mapping
|
||||
|
||||
ogg_packet op;
|
||||
op.packet = header;
|
||||
op.bytes = 19;
|
||||
op.b_o_s = 1;
|
||||
op.e_o_s = 0;
|
||||
op.granulepos = 0;
|
||||
op.packetno = 0;
|
||||
ogg_stream_packetin(&os, &op);
|
||||
|
||||
ogg_page og;
|
||||
while (ogg_stream_flush(&os, &og)) {
|
||||
outfile.write(reinterpret_cast<char*>(og.header), og.header_len);
|
||||
outfile.write(reinterpret_cast<char*>(og.body), og.body_len);
|
||||
}
|
||||
|
||||
// Write Opus comment header
|
||||
unsigned char comment[27];
|
||||
memcpy(comment, "OpusTags", 8);
|
||||
uint32_t vendor_len = 11;
|
||||
memcpy(&comment[8], &vendor_len, 4);
|
||||
memcpy(&comment[12], "SecondVoice", 11);
|
||||
uint32_t num_comments = 0;
|
||||
memcpy(&comment[23], &num_comments, 4);
|
||||
|
||||
op.packet = comment;
|
||||
op.bytes = 27;
|
||||
op.b_o_s = 0;
|
||||
op.e_o_s = 0;
|
||||
op.granulepos = 0;
|
||||
op.packetno = 1;
|
||||
ogg_stream_packetin(&os, &op);
|
||||
|
||||
while (ogg_stream_flush(&os, &og)) {
|
||||
outfile.write(reinterpret_cast<char*>(og.header), og.header_len);
|
||||
outfile.write(reinterpret_cast<char*>(og.body), og.body_len);
|
||||
}
|
||||
|
||||
// Encode audio frames
|
||||
const int frame_size = sample_rate / 50; // 20ms frames
|
||||
std::vector<unsigned char> opus_buffer(4000);
|
||||
int64_t granulepos = 0;
|
||||
int packetno = 2;
|
||||
|
||||
for (size_t i = 0; i < audio_data.size(); i += frame_size * channels) {
|
||||
size_t remaining = audio_data.size() - i;
|
||||
size_t samples_to_encode = std::min(static_cast<size_t>(frame_size * channels), remaining);
|
||||
|
||||
// Pad with zeros if needed
|
||||
std::vector<float> frame(frame_size * channels, 0.0f);
|
||||
std::copy(audio_data.begin() + i, audio_data.begin() + i + samples_to_encode, frame.begin());
|
||||
|
||||
int encoded_bytes = opus_encode_float(encoder, frame.data(), frame_size,
|
||||
opus_buffer.data(), opus_buffer.size());
|
||||
|
||||
if (encoded_bytes < 0) {
|
||||
std::cerr << "[SessionLogger] Opus encode error: " << opus_strerror(encoded_bytes) << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
granulepos += frame_size;
|
||||
bool is_last = (i + frame_size * channels >= audio_data.size());
|
||||
|
||||
op.packet = opus_buffer.data();
|
||||
op.bytes = encoded_bytes;
|
||||
op.b_o_s = 0;
|
||||
op.e_o_s = is_last ? 1 : 0;
|
||||
op.granulepos = granulepos;
|
||||
op.packetno = packetno++;
|
||||
ogg_stream_packetin(&os, &op);
|
||||
|
||||
while (is_last ? ogg_stream_flush(&os, &og) : ogg_stream_pageout(&os, &og)) {
|
||||
outfile.write(reinterpret_cast<char*>(og.header), og.header_len);
|
||||
outfile.write(reinterpret_cast<char*>(og.body), og.body_len);
|
||||
}
|
||||
}
|
||||
|
||||
ogg_stream_clear(&os);
|
||||
opus_encoder_destroy(encoder);
|
||||
outfile.close();
|
||||
|
||||
std::cout << "[SessionLogger] Saved audio: " << filepath << std::endl;
|
||||
return filename;
|
||||
}
|
||||
|
||||
void SessionLogger::setVadSettings(float rms_thresh, float peak_thresh) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
metadata_.vad_rms_threshold = rms_thresh;
|
||||
metadata_.vad_peak_threshold = peak_thresh;
|
||||
}
|
||||
|
||||
void SessionLogger::setModels(const std::string& whisper_model, const std::string& claude_model) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
metadata_.whisper_model = whisper_model;
|
||||
metadata_.claude_model = claude_model;
|
||||
}
|
||||
|
||||
std::vector<std::string> SessionLogger::getRecentTranscriptions(int count) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
std::vector<std::string> recent;
|
||||
int start = std::max(0, static_cast<int>(segments_.size()) - count);
|
||||
|
||||
for (int i = start; i < static_cast<int>(segments_.size()); ++i) {
|
||||
if (!segments_[i].was_filtered && !segments_[i].chinese.empty()) {
|
||||
recent.push_back(segments_[i].chinese);
|
||||
}
|
||||
}
|
||||
|
||||
return recent;
|
||||
}
|
||||
|
||||
void SessionLogger::writeSessionJson() {
|
||||
std::string filepath = session_path_ + "/session.json";
|
||||
std::ofstream file(filepath, std::ios::out | std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "[SessionLogger] Failed to write session.json" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Write UTF-8 BOM
|
||||
file << "\xEF\xBB\xBF";
|
||||
|
||||
// Manual JSON construction (to avoid extra dependencies)
|
||||
file << "{\n";
|
||||
file << " \"session_id\": \"" << metadata_.session_id << "\",\n";
|
||||
file << " \"start_time\": \"" << formatTimestamp(metadata_.start_time) << "\",\n";
|
||||
file << " \"end_time\": \"" << formatTimestamp(metadata_.end_time) << "\",\n";
|
||||
file << " \"total_segments\": " << metadata_.total_segments << ",\n";
|
||||
file << " \"filtered_segments\": " << metadata_.filtered_segments << ",\n";
|
||||
file << " \"total_audio_seconds\": " << std::fixed << std::setprecision(2) << metadata_.total_audio_seconds << ",\n";
|
||||
file << " \"total_cost_estimate\": " << std::fixed << std::setprecision(4) << metadata_.total_cost_estimate << ",\n";
|
||||
file << " \"vad_settings\": {\n";
|
||||
file << " \"rms_threshold\": " << std::fixed << std::setprecision(4) << metadata_.vad_rms_threshold << ",\n";
|
||||
file << " \"peak_threshold\": " << std::fixed << std::setprecision(4) << metadata_.vad_peak_threshold << "\n";
|
||||
file << " },\n";
|
||||
file << " \"models\": {\n";
|
||||
file << " \"whisper\": \"" << metadata_.whisper_model << "\",\n";
|
||||
file << " \"claude\": \"" << metadata_.claude_model << "\"\n";
|
||||
file << " }\n";
|
||||
file << "}\n";
|
||||
|
||||
file.close();
|
||||
std::cout << "[SessionLogger] Wrote " << filepath << std::endl;
|
||||
}
|
||||
|
||||
void SessionLogger::writeSegmentJson(const SegmentData& segment) {
|
||||
std::stringstream filename_ss;
|
||||
filename_ss << segments_path_ << "/" << std::setfill('0') << std::setw(3) << segment.id << ".json";
|
||||
std::string filepath = filename_ss.str();
|
||||
|
||||
std::ofstream file(filepath, std::ios::out | std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "[SessionLogger] Failed to write segment JSON: " << filepath << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Write UTF-8 BOM
|
||||
file << "\xEF\xBB\xBF";
|
||||
|
||||
// Escape JSON strings
|
||||
auto escapeJson = [](const std::string& s) -> std::string {
|
||||
std::string result;
|
||||
for (char c : s) {
|
||||
switch (c) {
|
||||
case '"': result += "\\\""; break;
|
||||
case '\\': result += "\\\\"; break;
|
||||
case '\n': result += "\\n"; break;
|
||||
case '\r': result += "\\r"; break;
|
||||
case '\t': result += "\\t"; break;
|
||||
default: result += c;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
file << "{\n";
|
||||
file << " \"id\": " << segment.id << ",\n";
|
||||
file << " \"chinese\": \"" << escapeJson(segment.chinese) << "\",\n";
|
||||
file << " \"french\": \"" << escapeJson(segment.french) << "\",\n";
|
||||
file << " \"audio\": {\n";
|
||||
file << " \"duration_seconds\": " << std::fixed << std::setprecision(3) << segment.duration_seconds << ",\n";
|
||||
file << " \"rms_level\": " << std::fixed << std::setprecision(4) << segment.rms_level << ",\n";
|
||||
file << " \"peak_level\": " << std::fixed << std::setprecision(4) << segment.peak_level << ",\n";
|
||||
file << " \"filename\": \"" << escapeJson(segment.audio_filename) << "\",\n";
|
||||
file << " \"hashes_per_second\": [";
|
||||
for (size_t i = 0; i < segment.audio_hashes.size(); ++i) {
|
||||
if (i > 0) file << ", ";
|
||||
file << "\"" << segment.audio_hashes[i] << "\"";
|
||||
}
|
||||
file << "]\n";
|
||||
file << " },\n";
|
||||
file << " \"timestamps\": {\n";
|
||||
file << " \"start\": \"" << formatTimestamp(segment.start_time) << "\",\n";
|
||||
file << " \"end\": \"" << formatTimestamp(segment.end_time) << "\"\n";
|
||||
file << " },\n";
|
||||
file << " \"processing\": {\n";
|
||||
file << " \"whisper_latency_ms\": " << std::fixed << std::setprecision(1) << segment.whisper_latency_ms << ",\n";
|
||||
file << " \"claude_latency_ms\": " << std::fixed << std::setprecision(1) << segment.claude_latency_ms << ",\n";
|
||||
file << " \"was_filtered\": " << (segment.was_filtered ? "true" : "false");
|
||||
if (segment.was_filtered) {
|
||||
file << ",\n \"filter_reason\": \"" << escapeJson(segment.filter_reason) << "\"";
|
||||
}
|
||||
file << "\n }\n";
|
||||
file << "}\n";
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
std::string SessionLogger::formatTimestamp(const std::chrono::system_clock::time_point& tp) const {
|
||||
auto time_t = std::chrono::system_clock::to_time_t(tp);
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(tp.time_since_epoch()) % 1000;
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(std::localtime(&time_t), "%Y-%m-%dT%H:%M:%S");
|
||||
ss << "." << std::setfill('0') << std::setw(3) << ms.count();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
} // namespace secondvoice
|
||||
104
src/utils/SessionLogger.h
Normal file
104
src/utils/SessionLogger.h
Normal file
@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
|
||||
namespace secondvoice {
|
||||
|
||||
struct SegmentData {
|
||||
int id;
|
||||
std::string chinese;
|
||||
std::string french;
|
||||
|
||||
// Audio metadata
|
||||
float duration_seconds;
|
||||
float rms_level;
|
||||
float peak_level;
|
||||
|
||||
// Timestamps
|
||||
std::chrono::system_clock::time_point start_time;
|
||||
std::chrono::system_clock::time_point end_time;
|
||||
|
||||
// Processing info
|
||||
float whisper_latency_ms;
|
||||
float claude_latency_ms;
|
||||
bool was_filtered;
|
||||
std::string filter_reason;
|
||||
|
||||
// Audio file (optional)
|
||||
std::string audio_filename;
|
||||
|
||||
// Audio fingerprint - hash per second for duplicate detection
|
||||
std::vector<std::string> audio_hashes;
|
||||
};
|
||||
|
||||
struct SessionMetadata {
|
||||
std::string session_id;
|
||||
std::chrono::system_clock::time_point start_time;
|
||||
std::chrono::system_clock::time_point end_time;
|
||||
|
||||
int total_segments;
|
||||
int filtered_segments;
|
||||
float total_audio_seconds;
|
||||
float total_cost_estimate;
|
||||
|
||||
// VAD settings used
|
||||
float vad_rms_threshold;
|
||||
float vad_peak_threshold;
|
||||
|
||||
// Models used
|
||||
std::string whisper_model;
|
||||
std::string claude_model;
|
||||
};
|
||||
|
||||
class SessionLogger {
|
||||
public:
|
||||
SessionLogger();
|
||||
~SessionLogger();
|
||||
|
||||
// Start a new session - creates directory structure
|
||||
bool startSession();
|
||||
|
||||
// End session - writes session.json
|
||||
void endSession();
|
||||
|
||||
// Log a segment (called after transcription+translation)
|
||||
void logSegment(const SegmentData& segment);
|
||||
|
||||
// Save audio data for a segment (returns filename)
|
||||
// Also calculates audio_hashes (one hash per second)
|
||||
std::string saveSegmentAudio(int segment_id, const std::vector<float>& audio_data,
|
||||
int sample_rate, int channels,
|
||||
std::vector<std::string>& out_hashes);
|
||||
|
||||
// Update session metadata
|
||||
void setVadSettings(float rms_thresh, float peak_thresh);
|
||||
void setModels(const std::string& whisper_model, const std::string& claude_model);
|
||||
|
||||
// Getters
|
||||
const std::string& getSessionPath() const { return session_path_; }
|
||||
int getNextSegmentId() const { return next_segment_id_; }
|
||||
|
||||
// Get last N transcriptions for context (for Whisper prompt)
|
||||
std::vector<std::string> getRecentTranscriptions(int count = 3) const;
|
||||
|
||||
private:
|
||||
void writeSessionJson();
|
||||
void writeSegmentJson(const SegmentData& segment);
|
||||
std::string formatTimestamp(const std::chrono::system_clock::time_point& tp) const;
|
||||
|
||||
std::string session_path_;
|
||||
std::string segments_path_;
|
||||
SessionMetadata metadata_;
|
||||
std::vector<SegmentData> segments_;
|
||||
|
||||
int next_segment_id_ = 1;
|
||||
bool session_active_ = false;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
|
||||
} // namespace secondvoice
|
||||
329
transcripts/transcript_20251123_193608.txt
Normal file
329
transcripts/transcript_20251123_193608.txt
Normal file
@ -0,0 +1,329 @@
|
||||
═══════════════════════════════════════════════════════════════
|
||||
SecondVoice - Transcript Export
|
||||
Date: 2025-11-23 19:36:08
|
||||
Duration: 5:31
|
||||
Segments: 75
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
TEXTE COMPLET / FULL TEXT
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
[中文 / Chinese]
|
||||
對,兩個都是。 两个老鼠求我 好的 去年都没有过呀。 还和我公司。 六啊七 你叫她。 面试就是最专业的。 2025年12月7号上海市公考。 狗年就是七号。 他不是经理转业。 我叫他去口。 我查到了。 他是比你。 我们学校有考点。 忘掉了。 是不是翻译不过来? 有人骂人了。 不過來。 还有因素呢。 昨天就是行了。 太多声音了。 有人骂人了。 他多大? Si je parle en français, ça va aussi mettre du français? 你是没有中文呀。 两个老鼠求我,好的,今年都没有怪。 我们是超频安的。 我只去那儿了。 打电话。 不懂。 我去那边的路啊。 你到时候考试是 要不要过去等你。 我上次就觉得。 你好。 not working 他这样反应不太好。 一個房。 你上次是什么时候? 什么时候考的? 你好。 汪汪汪汪。 Je suis la meilleure. 那你還看不到。 起来 你不是直接把她清理的吗? 好些。 你好。 你好。 妈说这是饭局吗? 你是做什么的? 去一个什么学校? 非常感谢你。 那说这是饭局吗? 很欣喜。 有没有个。 对啊。 网路。 我還沒。 你好吗? 我没。 有什么音乐。 医院。 你也明白。 我没付钱。 来。 我在用 online API。 你要不要喝? 去里面找找呀。 你是我。 要走嗎? 我很。 的。 我打算应该下个月。
|
||||
|
||||
[Français / French]
|
||||
Oui, les deux le sont. Deux souris me supplient D'accord L'année dernière, il n'y en avait pas du tout. Il travaille encore dans ma société. Six et sept Appelle-la. L'entretien est le plus professionnel. Le 7 décembre 2025, examen de la fonction publique à Shanghai. L'année du chien est le sept. Il n'est pas un ancien militaire devenu gestionnaire. Je lui ai demandé d'aller [contenu offensant]. J'ai trouvé. Il est mieux que toi. Notre école est un centre d'examen. J'ai oublié. Est-ce que c'est intraduisible ? Quelqu'un a insulté quelqu'un. Ne viens pas. Il y a d'autres facteurs. Hier, c'était bon. Il y a trop de bruit. Quelqu'un a insulté quelqu'un. Quel âge a-t-il ? Si je parle en français, ça va aussi mettre du français ? Tu ne sais pas parler chinois. Deux souris m'ont supplié, d'accord, cette année il n'y aura pas de problèmes. Nous sommes Superpower. Je n'y suis allé qu'une seule fois. Passer un coup de téléphone. Je ne comprends pas. Je vais par là-bas. Tu passeras l'examen à ce moment-là Je vais t'attendre là-bas. Je pensais déjà la dernière fois. Bonjour. Ne fonctionne pas Il réagit de manière pas très appropriée. Une chambre. Quand étais-tu la dernière fois ? Quand est-ce que tu as passé l'examen ? Bonjour. Ouaf ouaf ouaf ouaf. Je suis la meilleure. Tu ne peux toujours pas le voir. Debout Tu ne l'as pas directement nettoyée ? Ça va mieux. Bonjour. Bonjour. Maman, est-ce que c'est un repas d'affaires ? Que fais-tu dans la vie ? À quelle école vas-tu ? Merci beaucoup. Est-ce qu'on peut appeler ça un repas d'affaires ? Je suis très heureux. Il y a-t-il un ? Oui, c'est ça. Réseau. Je n'ai pas encore. Comment vas-tu ? Je n'ai pas. Quel type de musique y a-t-il ? Hôpital. Tu comprends aussi. Je n'ai pas payé. Viens. Je suis en train d'utiliser une API en ligne. Veux-tu boire ? Vas voir à l'intérieur. Tu es moi. Veux-tu partir ? Je suis. De. Je prévois de le faire le mois prochain.
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
SEGMENTS DÉTAILLÉS / DETAILED SEGMENTS
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
[Segment 1]
|
||||
中文: 對,兩個都是。
|
||||
FR: Oui, les deux le sont.
|
||||
|
||||
[Segment 2]
|
||||
中文: 两个老鼠求我
|
||||
FR: Deux souris me supplient
|
||||
|
||||
[Segment 3]
|
||||
中文: 好的
|
||||
FR: D'accord
|
||||
|
||||
[Segment 4]
|
||||
中文: 去年都没有过呀。
|
||||
FR: L'année dernière, il n'y en avait pas du tout.
|
||||
|
||||
[Segment 5]
|
||||
中文: 还和我公司。
|
||||
FR: Il travaille encore dans ma société.
|
||||
|
||||
[Segment 6]
|
||||
中文: 六啊七
|
||||
FR: Six et sept
|
||||
|
||||
[Segment 7]
|
||||
中文: 你叫她。
|
||||
FR: Appelle-la.
|
||||
|
||||
[Segment 8]
|
||||
中文: 面试就是最专业的。
|
||||
FR: L'entretien est le plus professionnel.
|
||||
|
||||
[Segment 9]
|
||||
中文: 2025年12月7号上海市公考。
|
||||
FR: Le 7 décembre 2025, examen de la fonction publique à Shanghai.
|
||||
|
||||
[Segment 10]
|
||||
中文: 狗年就是七号。
|
||||
FR: L'année du chien est le sept.
|
||||
|
||||
[Segment 11]
|
||||
中文: 他不是经理转业。
|
||||
FR: Il n'est pas un ancien militaire devenu gestionnaire.
|
||||
|
||||
[Segment 12]
|
||||
中文: 我叫他去口。
|
||||
FR: Je lui ai demandé d'aller [contenu offensant].
|
||||
|
||||
[Segment 13]
|
||||
中文: 我查到了。
|
||||
FR: J'ai trouvé.
|
||||
|
||||
[Segment 14]
|
||||
中文: 他是比你。
|
||||
FR: Il est mieux que toi.
|
||||
|
||||
[Segment 15]
|
||||
中文: 我们学校有考点。
|
||||
FR: Notre école est un centre d'examen.
|
||||
|
||||
[Segment 16]
|
||||
中文: 忘掉了。
|
||||
FR: J'ai oublié.
|
||||
|
||||
[Segment 17]
|
||||
中文: 是不是翻译不过来?
|
||||
FR: Est-ce que c'est intraduisible ?
|
||||
|
||||
[Segment 18]
|
||||
中文: 有人骂人了。
|
||||
FR: Quelqu'un a insulté quelqu'un.
|
||||
|
||||
[Segment 19]
|
||||
中文: 不過來。
|
||||
FR: Ne viens pas.
|
||||
|
||||
[Segment 20]
|
||||
中文: 还有因素呢。
|
||||
FR: Il y a d'autres facteurs.
|
||||
|
||||
[Segment 21]
|
||||
中文: 昨天就是行了。
|
||||
FR: Hier, c'était bon.
|
||||
|
||||
[Segment 22]
|
||||
中文: 太多声音了。
|
||||
FR: Il y a trop de bruit.
|
||||
|
||||
[Segment 23]
|
||||
中文: 有人骂人了。
|
||||
FR: Quelqu'un a insulté quelqu'un.
|
||||
|
||||
[Segment 24]
|
||||
中文: 他多大?
|
||||
FR: Quel âge a-t-il ?
|
||||
|
||||
[Segment 25]
|
||||
中文: Si je parle en français, ça va aussi mettre du français?
|
||||
FR: Si je parle en français, ça va aussi mettre du français ?
|
||||
|
||||
[Segment 26]
|
||||
中文: 你是没有中文呀。
|
||||
FR: Tu ne sais pas parler chinois.
|
||||
|
||||
[Segment 27]
|
||||
中文: 两个老鼠求我,好的,今年都没有怪。
|
||||
FR: Deux souris m'ont supplié, d'accord, cette année il n'y aura pas de problèmes.
|
||||
|
||||
[Segment 28]
|
||||
中文: 我们是超频安的。
|
||||
FR: Nous sommes Superpower.
|
||||
|
||||
[Segment 29]
|
||||
中文: 我只去那儿了。
|
||||
FR: Je n'y suis allé qu'une seule fois.
|
||||
|
||||
[Segment 30]
|
||||
中文: 打电话。
|
||||
FR: Passer un coup de téléphone.
|
||||
|
||||
[Segment 31]
|
||||
中文: 不懂。
|
||||
FR: Je ne comprends pas.
|
||||
|
||||
[Segment 32]
|
||||
中文: 我去那边的路啊。
|
||||
FR: Je vais par là-bas.
|
||||
|
||||
[Segment 33]
|
||||
中文: 你到时候考试是
|
||||
FR: Tu passeras l'examen à ce moment-là
|
||||
|
||||
[Segment 34]
|
||||
中文: 要不要过去等你。
|
||||
FR: Je vais t'attendre là-bas.
|
||||
|
||||
[Segment 35]
|
||||
中文: 我上次就觉得。
|
||||
FR: Je pensais déjà la dernière fois.
|
||||
|
||||
[Segment 36]
|
||||
中文: 你好。
|
||||
FR: Bonjour.
|
||||
|
||||
[Segment 37]
|
||||
中文: not working
|
||||
FR: Ne fonctionne pas
|
||||
|
||||
[Segment 38]
|
||||
中文: 他这样反应不太好。
|
||||
FR: Il réagit de manière pas très appropriée.
|
||||
|
||||
[Segment 39]
|
||||
中文: 一個房。
|
||||
FR: Une chambre.
|
||||
|
||||
[Segment 40]
|
||||
中文: 你上次是什么时候?
|
||||
FR: Quand étais-tu la dernière fois ?
|
||||
|
||||
[Segment 41]
|
||||
中文: 什么时候考的?
|
||||
FR: Quand est-ce que tu as passé l'examen ?
|
||||
|
||||
[Segment 42]
|
||||
中文: 你好。
|
||||
FR: Bonjour.
|
||||
|
||||
[Segment 43]
|
||||
中文: 汪汪汪汪。
|
||||
FR: Ouaf ouaf ouaf ouaf.
|
||||
|
||||
[Segment 44]
|
||||
中文: Je suis la meilleure.
|
||||
FR: Je suis la meilleure.
|
||||
|
||||
[Segment 45]
|
||||
中文: 那你還看不到。
|
||||
FR: Tu ne peux toujours pas le voir.
|
||||
|
||||
[Segment 46]
|
||||
中文: 起来
|
||||
FR: Debout
|
||||
|
||||
[Segment 47]
|
||||
中文: 你不是直接把她清理的吗?
|
||||
FR: Tu ne l'as pas directement nettoyée ?
|
||||
|
||||
[Segment 48]
|
||||
中文: 好些。
|
||||
FR: Ça va mieux.
|
||||
|
||||
[Segment 49]
|
||||
中文: 你好。
|
||||
FR: Bonjour.
|
||||
|
||||
[Segment 50]
|
||||
中文: 你好。
|
||||
FR: Bonjour.
|
||||
|
||||
[Segment 51]
|
||||
中文: 妈说这是饭局吗?
|
||||
FR: Maman, est-ce que c'est un repas d'affaires ?
|
||||
|
||||
[Segment 52]
|
||||
中文: 你是做什么的?
|
||||
FR: Que fais-tu dans la vie ?
|
||||
|
||||
[Segment 53]
|
||||
中文: 去一个什么学校?
|
||||
FR: À quelle école vas-tu ?
|
||||
|
||||
[Segment 54]
|
||||
中文: 非常感谢你。
|
||||
FR: Merci beaucoup.
|
||||
|
||||
[Segment 55]
|
||||
中文: 那说这是饭局吗?
|
||||
FR: Est-ce qu'on peut appeler ça un repas d'affaires ?
|
||||
|
||||
[Segment 56]
|
||||
中文: 很欣喜。
|
||||
FR: Je suis très heureux.
|
||||
|
||||
[Segment 57]
|
||||
中文: 有没有个。
|
||||
FR: Il y a-t-il un ?
|
||||
|
||||
[Segment 58]
|
||||
中文: 对啊。
|
||||
FR: Oui, c'est ça.
|
||||
|
||||
[Segment 59]
|
||||
中文: 网路。
|
||||
FR: Réseau.
|
||||
|
||||
[Segment 60]
|
||||
中文: 我還沒。
|
||||
FR: Je n'ai pas encore.
|
||||
|
||||
[Segment 61]
|
||||
中文: 你好吗?
|
||||
FR: Comment vas-tu ?
|
||||
|
||||
[Segment 62]
|
||||
中文: 我没。
|
||||
FR: Je n'ai pas.
|
||||
|
||||
[Segment 63]
|
||||
中文: 有什么音乐。
|
||||
FR: Quel type de musique y a-t-il ?
|
||||
|
||||
[Segment 64]
|
||||
中文: 医院。
|
||||
FR: Hôpital.
|
||||
|
||||
[Segment 65]
|
||||
中文: 你也明白。
|
||||
FR: Tu comprends aussi.
|
||||
|
||||
[Segment 66]
|
||||
中文: 我没付钱。
|
||||
FR: Je n'ai pas payé.
|
||||
|
||||
[Segment 67]
|
||||
中文: 来。
|
||||
FR: Viens.
|
||||
|
||||
[Segment 68]
|
||||
中文: 我在用 online API。
|
||||
FR: Je suis en train d'utiliser une API en ligne.
|
||||
|
||||
[Segment 69]
|
||||
中文: 你要不要喝?
|
||||
FR: Veux-tu boire ?
|
||||
|
||||
[Segment 70]
|
||||
中文: 去里面找找呀。
|
||||
FR: Vas voir à l'intérieur.
|
||||
|
||||
[Segment 71]
|
||||
中文: 你是我。
|
||||
FR: Tu es moi.
|
||||
|
||||
[Segment 72]
|
||||
中文: 要走嗎?
|
||||
FR: Veux-tu partir ?
|
||||
|
||||
[Segment 73]
|
||||
中文: 我很。
|
||||
FR: Je suis.
|
||||
|
||||
[Segment 74]
|
||||
中文: 的。
|
||||
FR: De.
|
||||
|
||||
[Segment 75]
|
||||
中文: 我打算应该下个月。
|
||||
FR: Je prévois de le faire le mois prochain.
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
STATISTIQUES / STATISTICS
|
||||
───────────────────────────────────────────────────────────────
|
||||
Audio processed: 212 seconds
|
||||
Whisper API calls: 75
|
||||
Claude API calls: 75
|
||||
Estimated cost: $0.0963
|
||||
═══════════════════════════════════════════════════════════════
|
||||
329
transcripts/transcript_20251123_193612.txt
Normal file
329
transcripts/transcript_20251123_193612.txt
Normal file
@ -0,0 +1,329 @@
|
||||
═══════════════════════════════════════════════════════════════
|
||||
SecondVoice - Transcript Export
|
||||
Date: 2025-11-23 19:36:12
|
||||
Duration: 5:34
|
||||
Segments: 75
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
TEXTE COMPLET / FULL TEXT
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
[中文 / Chinese]
|
||||
對,兩個都是。 两个老鼠求我 好的 去年都没有过呀。 还和我公司。 六啊七 你叫她。 面试就是最专业的。 2025年12月7号上海市公考。 狗年就是七号。 他不是经理转业。 我叫他去口。 我查到了。 他是比你。 我们学校有考点。 忘掉了。 是不是翻译不过来? 有人骂人了。 不過來。 还有因素呢。 昨天就是行了。 太多声音了。 有人骂人了。 他多大? Si je parle en français, ça va aussi mettre du français? 你是没有中文呀。 两个老鼠求我,好的,今年都没有怪。 我们是超频安的。 我只去那儿了。 打电话。 不懂。 我去那边的路啊。 你到时候考试是 要不要过去等你。 我上次就觉得。 你好。 not working 他这样反应不太好。 一個房。 你上次是什么时候? 什么时候考的? 你好。 汪汪汪汪。 Je suis la meilleure. 那你還看不到。 起来 你不是直接把她清理的吗? 好些。 你好。 你好。 妈说这是饭局吗? 你是做什么的? 去一个什么学校? 非常感谢你。 那说这是饭局吗? 很欣喜。 有没有个。 对啊。 网路。 我還沒。 你好吗? 我没。 有什么音乐。 医院。 你也明白。 我没付钱。 来。 我在用 online API。 你要不要喝? 去里面找找呀。 你是我。 要走嗎? 我很。 的。 我打算应该下个月。
|
||||
|
||||
[Français / French]
|
||||
Oui, les deux le sont. Deux souris me supplient D'accord L'année dernière, il n'y en avait pas du tout. Il travaille encore dans ma société. Six et sept Appelle-la. L'entretien est le plus professionnel. Le 7 décembre 2025, examen de la fonction publique à Shanghai. L'année du chien est le sept. Il n'est pas un ancien militaire devenu gestionnaire. Je lui ai demandé d'aller [contenu offensant]. J'ai trouvé. Il est mieux que toi. Notre école est un centre d'examen. J'ai oublié. Est-ce que c'est intraduisible ? Quelqu'un a insulté quelqu'un. Ne viens pas. Il y a d'autres facteurs. Hier, c'était bon. Il y a trop de bruit. Quelqu'un a insulté quelqu'un. Quel âge a-t-il ? Si je parle en français, ça va aussi mettre du français ? Tu ne sais pas parler chinois. Deux souris m'ont supplié, d'accord, cette année il n'y aura pas de problèmes. Nous sommes Superpower. Je n'y suis allé qu'une seule fois. Passer un coup de téléphone. Je ne comprends pas. Je vais par là-bas. Tu passeras l'examen à ce moment-là Je vais t'attendre là-bas. Je pensais déjà la dernière fois. Bonjour. Ne fonctionne pas Il réagit de manière pas très appropriée. Une chambre. Quand étais-tu la dernière fois ? Quand est-ce que tu as passé l'examen ? Bonjour. Ouaf ouaf ouaf ouaf. Je suis la meilleure. Tu ne peux toujours pas le voir. Debout Tu ne l'as pas directement nettoyée ? Ça va mieux. Bonjour. Bonjour. Maman, est-ce que c'est un repas d'affaires ? Que fais-tu dans la vie ? À quelle école vas-tu ? Merci beaucoup. Est-ce qu'on peut appeler ça un repas d'affaires ? Je suis très heureux. Il y a-t-il un ? Oui, c'est ça. Réseau. Je n'ai pas encore. Comment vas-tu ? Je n'ai pas. Quel type de musique y a-t-il ? Hôpital. Tu comprends aussi. Je n'ai pas payé. Viens. Je suis en train d'utiliser une API en ligne. Veux-tu boire ? Vas voir à l'intérieur. Tu es moi. Veux-tu partir ? Je suis. De. Je prévois de le faire le mois prochain.
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
SEGMENTS DÉTAILLÉS / DETAILED SEGMENTS
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
[Segment 1]
|
||||
中文: 對,兩個都是。
|
||||
FR: Oui, les deux le sont.
|
||||
|
||||
[Segment 2]
|
||||
中文: 两个老鼠求我
|
||||
FR: Deux souris me supplient
|
||||
|
||||
[Segment 3]
|
||||
中文: 好的
|
||||
FR: D'accord
|
||||
|
||||
[Segment 4]
|
||||
中文: 去年都没有过呀。
|
||||
FR: L'année dernière, il n'y en avait pas du tout.
|
||||
|
||||
[Segment 5]
|
||||
中文: 还和我公司。
|
||||
FR: Il travaille encore dans ma société.
|
||||
|
||||
[Segment 6]
|
||||
中文: 六啊七
|
||||
FR: Six et sept
|
||||
|
||||
[Segment 7]
|
||||
中文: 你叫她。
|
||||
FR: Appelle-la.
|
||||
|
||||
[Segment 8]
|
||||
中文: 面试就是最专业的。
|
||||
FR: L'entretien est le plus professionnel.
|
||||
|
||||
[Segment 9]
|
||||
中文: 2025年12月7号上海市公考。
|
||||
FR: Le 7 décembre 2025, examen de la fonction publique à Shanghai.
|
||||
|
||||
[Segment 10]
|
||||
中文: 狗年就是七号。
|
||||
FR: L'année du chien est le sept.
|
||||
|
||||
[Segment 11]
|
||||
中文: 他不是经理转业。
|
||||
FR: Il n'est pas un ancien militaire devenu gestionnaire.
|
||||
|
||||
[Segment 12]
|
||||
中文: 我叫他去口。
|
||||
FR: Je lui ai demandé d'aller [contenu offensant].
|
||||
|
||||
[Segment 13]
|
||||
中文: 我查到了。
|
||||
FR: J'ai trouvé.
|
||||
|
||||
[Segment 14]
|
||||
中文: 他是比你。
|
||||
FR: Il est mieux que toi.
|
||||
|
||||
[Segment 15]
|
||||
中文: 我们学校有考点。
|
||||
FR: Notre école est un centre d'examen.
|
||||
|
||||
[Segment 16]
|
||||
中文: 忘掉了。
|
||||
FR: J'ai oublié.
|
||||
|
||||
[Segment 17]
|
||||
中文: 是不是翻译不过来?
|
||||
FR: Est-ce que c'est intraduisible ?
|
||||
|
||||
[Segment 18]
|
||||
中文: 有人骂人了。
|
||||
FR: Quelqu'un a insulté quelqu'un.
|
||||
|
||||
[Segment 19]
|
||||
中文: 不過來。
|
||||
FR: Ne viens pas.
|
||||
|
||||
[Segment 20]
|
||||
中文: 还有因素呢。
|
||||
FR: Il y a d'autres facteurs.
|
||||
|
||||
[Segment 21]
|
||||
中文: 昨天就是行了。
|
||||
FR: Hier, c'était bon.
|
||||
|
||||
[Segment 22]
|
||||
中文: 太多声音了。
|
||||
FR: Il y a trop de bruit.
|
||||
|
||||
[Segment 23]
|
||||
中文: 有人骂人了。
|
||||
FR: Quelqu'un a insulté quelqu'un.
|
||||
|
||||
[Segment 24]
|
||||
中文: 他多大?
|
||||
FR: Quel âge a-t-il ?
|
||||
|
||||
[Segment 25]
|
||||
中文: Si je parle en français, ça va aussi mettre du français?
|
||||
FR: Si je parle en français, ça va aussi mettre du français ?
|
||||
|
||||
[Segment 26]
|
||||
中文: 你是没有中文呀。
|
||||
FR: Tu ne sais pas parler chinois.
|
||||
|
||||
[Segment 27]
|
||||
中文: 两个老鼠求我,好的,今年都没有怪。
|
||||
FR: Deux souris m'ont supplié, d'accord, cette année il n'y aura pas de problèmes.
|
||||
|
||||
[Segment 28]
|
||||
中文: 我们是超频安的。
|
||||
FR: Nous sommes Superpower.
|
||||
|
||||
[Segment 29]
|
||||
中文: 我只去那儿了。
|
||||
FR: Je n'y suis allé qu'une seule fois.
|
||||
|
||||
[Segment 30]
|
||||
中文: 打电话。
|
||||
FR: Passer un coup de téléphone.
|
||||
|
||||
[Segment 31]
|
||||
中文: 不懂。
|
||||
FR: Je ne comprends pas.
|
||||
|
||||
[Segment 32]
|
||||
中文: 我去那边的路啊。
|
||||
FR: Je vais par là-bas.
|
||||
|
||||
[Segment 33]
|
||||
中文: 你到时候考试是
|
||||
FR: Tu passeras l'examen à ce moment-là
|
||||
|
||||
[Segment 34]
|
||||
中文: 要不要过去等你。
|
||||
FR: Je vais t'attendre là-bas.
|
||||
|
||||
[Segment 35]
|
||||
中文: 我上次就觉得。
|
||||
FR: Je pensais déjà la dernière fois.
|
||||
|
||||
[Segment 36]
|
||||
中文: 你好。
|
||||
FR: Bonjour.
|
||||
|
||||
[Segment 37]
|
||||
中文: not working
|
||||
FR: Ne fonctionne pas
|
||||
|
||||
[Segment 38]
|
||||
中文: 他这样反应不太好。
|
||||
FR: Il réagit de manière pas très appropriée.
|
||||
|
||||
[Segment 39]
|
||||
中文: 一個房。
|
||||
FR: Une chambre.
|
||||
|
||||
[Segment 40]
|
||||
中文: 你上次是什么时候?
|
||||
FR: Quand étais-tu la dernière fois ?
|
||||
|
||||
[Segment 41]
|
||||
中文: 什么时候考的?
|
||||
FR: Quand est-ce que tu as passé l'examen ?
|
||||
|
||||
[Segment 42]
|
||||
中文: 你好。
|
||||
FR: Bonjour.
|
||||
|
||||
[Segment 43]
|
||||
中文: 汪汪汪汪。
|
||||
FR: Ouaf ouaf ouaf ouaf.
|
||||
|
||||
[Segment 44]
|
||||
中文: Je suis la meilleure.
|
||||
FR: Je suis la meilleure.
|
||||
|
||||
[Segment 45]
|
||||
中文: 那你還看不到。
|
||||
FR: Tu ne peux toujours pas le voir.
|
||||
|
||||
[Segment 46]
|
||||
中文: 起来
|
||||
FR: Debout
|
||||
|
||||
[Segment 47]
|
||||
中文: 你不是直接把她清理的吗?
|
||||
FR: Tu ne l'as pas directement nettoyée ?
|
||||
|
||||
[Segment 48]
|
||||
中文: 好些。
|
||||
FR: Ça va mieux.
|
||||
|
||||
[Segment 49]
|
||||
中文: 你好。
|
||||
FR: Bonjour.
|
||||
|
||||
[Segment 50]
|
||||
中文: 你好。
|
||||
FR: Bonjour.
|
||||
|
||||
[Segment 51]
|
||||
中文: 妈说这是饭局吗?
|
||||
FR: Maman, est-ce que c'est un repas d'affaires ?
|
||||
|
||||
[Segment 52]
|
||||
中文: 你是做什么的?
|
||||
FR: Que fais-tu dans la vie ?
|
||||
|
||||
[Segment 53]
|
||||
中文: 去一个什么学校?
|
||||
FR: À quelle école vas-tu ?
|
||||
|
||||
[Segment 54]
|
||||
中文: 非常感谢你。
|
||||
FR: Merci beaucoup.
|
||||
|
||||
[Segment 55]
|
||||
中文: 那说这是饭局吗?
|
||||
FR: Est-ce qu'on peut appeler ça un repas d'affaires ?
|
||||
|
||||
[Segment 56]
|
||||
中文: 很欣喜。
|
||||
FR: Je suis très heureux.
|
||||
|
||||
[Segment 57]
|
||||
中文: 有没有个。
|
||||
FR: Il y a-t-il un ?
|
||||
|
||||
[Segment 58]
|
||||
中文: 对啊。
|
||||
FR: Oui, c'est ça.
|
||||
|
||||
[Segment 59]
|
||||
中文: 网路。
|
||||
FR: Réseau.
|
||||
|
||||
[Segment 60]
|
||||
中文: 我還沒。
|
||||
FR: Je n'ai pas encore.
|
||||
|
||||
[Segment 61]
|
||||
中文: 你好吗?
|
||||
FR: Comment vas-tu ?
|
||||
|
||||
[Segment 62]
|
||||
中文: 我没。
|
||||
FR: Je n'ai pas.
|
||||
|
||||
[Segment 63]
|
||||
中文: 有什么音乐。
|
||||
FR: Quel type de musique y a-t-il ?
|
||||
|
||||
[Segment 64]
|
||||
中文: 医院。
|
||||
FR: Hôpital.
|
||||
|
||||
[Segment 65]
|
||||
中文: 你也明白。
|
||||
FR: Tu comprends aussi.
|
||||
|
||||
[Segment 66]
|
||||
中文: 我没付钱。
|
||||
FR: Je n'ai pas payé.
|
||||
|
||||
[Segment 67]
|
||||
中文: 来。
|
||||
FR: Viens.
|
||||
|
||||
[Segment 68]
|
||||
中文: 我在用 online API。
|
||||
FR: Je suis en train d'utiliser une API en ligne.
|
||||
|
||||
[Segment 69]
|
||||
中文: 你要不要喝?
|
||||
FR: Veux-tu boire ?
|
||||
|
||||
[Segment 70]
|
||||
中文: 去里面找找呀。
|
||||
FR: Vas voir à l'intérieur.
|
||||
|
||||
[Segment 71]
|
||||
中文: 你是我。
|
||||
FR: Tu es moi.
|
||||
|
||||
[Segment 72]
|
||||
中文: 要走嗎?
|
||||
FR: Veux-tu partir ?
|
||||
|
||||
[Segment 73]
|
||||
中文: 我很。
|
||||
FR: Je suis.
|
||||
|
||||
[Segment 74]
|
||||
中文: 的。
|
||||
FR: De.
|
||||
|
||||
[Segment 75]
|
||||
中文: 我打算应该下个月。
|
||||
FR: Je prévois de le faire le mois prochain.
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
STATISTIQUES / STATISTICS
|
||||
───────────────────────────────────────────────────────────────
|
||||
Audio processed: 212 seconds
|
||||
Whisper API calls: 75
|
||||
Claude API calls: 75
|
||||
Estimated cost: $0.0963
|
||||
═══════════════════════════════════════════════════════════════
|
||||
41
transcripts/transcript_20251124_083029.txt
Normal file
41
transcripts/transcript_20251124_083029.txt
Normal file
@ -0,0 +1,41 @@
|
||||
═══════════════════════════════════════════════════════════════
|
||||
SecondVoice - Transcript Export
|
||||
Date: 2025-11-24 08:30:29
|
||||
Duration: 0:46
|
||||
Segments: 3
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
TEXTE COMPLET / FULL TEXT
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
[中文 / Chinese]
|
||||
我很忙。 你会说英文吗? 我也不知道。
|
||||
|
||||
[Français / French]
|
||||
Je suis très occupé. Parles-tu anglais ? Je ne sais pas non plus.
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
SEGMENTS DÉTAILLÉS / DETAILED SEGMENTS
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
[Segment 1]
|
||||
中文: 我很忙。
|
||||
FR: Je suis très occupé.
|
||||
|
||||
[Segment 2]
|
||||
中文: 你会说英文吗?
|
||||
FR: Parles-tu anglais ?
|
||||
|
||||
[Segment 3]
|
||||
中文: 我也不知道。
|
||||
FR: Je ne sais pas non plus.
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
STATISTIQUES / STATISTICS
|
||||
───────────────────────────────────────────────────────────────
|
||||
Audio processed: 3 seconds
|
||||
Whisper API calls: 3
|
||||
Claude API calls: 3
|
||||
Estimated cost: $0.0033
|
||||
═══════════════════════════════════════════════════════════════
|
||||
Loading…
Reference in New Issue
Block a user