Compare commits
2 Commits
feature/se
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1db83b7bce | |||
| 3ec2a8beca |
1
.gitignore
vendored
1
.gitignore
vendored
@ -68,6 +68,7 @@ sessions/
|
|||||||
|
|
||||||
# Claude Code local settings
|
# Claude Code local settings
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
.claudiomiro/
|
||||||
|
|
||||||
# Build scripts (local)
|
# Build scripts (local)
|
||||||
run_build.ps1
|
run_build.ps1
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"model": "gpt-4o-mini-transcribe",
|
"model": "gpt-4o-mini-transcribe",
|
||||||
"language": "zh",
|
"language": "zh",
|
||||||
"temperature": 0.0,
|
"temperature": 0.0,
|
||||||
"prompt": "Transcription en direct d'une conversation en chinois mandarin. Plusieurs interlocuteurs parlent, parfois en même temps. RÈGLES STRICTES: (1) Ne transcris QUE les paroles audibles en chinois. (2) Si l'audio est inaudible, du bruit, ou du silence, renvoie une chaîne vide. (3) NE GÉNÈRE JAMAIS ces phrases: 谢谢观看, 感谢收看, 订阅, 请订阅, 下期再见, Thank you, Subscribe, 字幕. (4) Ignore: musique, applaudissements, rires, bruits de fond, respirations.",
|
"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,
|
"stream": false,
|
||||||
"response_format": "text"
|
"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);
|
const float* in = static_cast<const float*>(input);
|
||||||
unsigned long sample_count = frame_count * self->channels_;
|
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 ===
|
// === REAL-TIME DENOISING ===
|
||||||
// Process audio through RNNoise in real-time for meter display
|
// Process audio through RNNoise in real-time for meter display
|
||||||
std::vector<float> denoised_samples;
|
std::vector<float> denoised_samples;
|
||||||
if (self->noise_reducer_ && self->noise_reducer_->isEnabled()) {
|
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_sum_squared = 0.0f;
|
||||||
float raw_max_amp = 0.0f;
|
float raw_max_amp = 0.0f;
|
||||||
for (unsigned long i = 0; i < sample_count; ++i) {
|
for (unsigned long i = 0; i < sample_count; ++i) {
|
||||||
float sample = in[i];
|
float sample = amplified_in[i];
|
||||||
raw_sum_squared += sample * sample;
|
raw_sum_squared += sample * sample;
|
||||||
if (std::abs(sample) > raw_max_amp) {
|
if (std::abs(sample) > raw_max_amp) {
|
||||||
raw_max_amp = std::abs(sample);
|
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(),
|
self->speech_buffer_.insert(self->speech_buffer_.end(),
|
||||||
denoised_samples.begin(), denoised_samples.end());
|
denoised_samples.begin(), denoised_samples.end());
|
||||||
} else {
|
} else {
|
||||||
// Fallback to raw if denoising disabled
|
// Fallback to amplified if denoising disabled
|
||||||
for (unsigned long i = 0; i < sample_count; ++i) {
|
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;
|
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 we were speaking and now have enough silence, flush
|
||||||
if (self->speech_buffer_.size() > 0) {
|
if (self->speech_buffer_.size() > 0) {
|
||||||
// Add trailing silence (denoised)
|
// Add trailing silence (denoised or amplified)
|
||||||
if (!denoised_samples.empty()) {
|
if (!denoised_samples.empty()) {
|
||||||
self->speech_buffer_.insert(self->speech_buffer_.end(),
|
self->speech_buffer_.insert(self->speech_buffer_.end(),
|
||||||
denoised_samples.begin(), denoised_samples.end());
|
denoised_samples.begin(), denoised_samples.end());
|
||||||
} else {
|
} else {
|
||||||
for (unsigned long i = 0; i < sample_count; ++i) {
|
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_rms_threshold_ = rms_threshold;
|
||||||
vad_peak_threshold_ = peak_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 setSilenceDuration(int ms) { silence_duration_ms_ = ms; }
|
||||||
void setMinSpeechDuration(int ms) { min_speech_duration_ms_ = ms; }
|
void setMinSpeechDuration(int ms) { min_speech_duration_ms_ = ms; }
|
||||||
void setMaxSpeechDuration(int ms) { max_speech_duration_ms_ = ms; }
|
void setMaxSpeechDuration(int ms) { max_speech_duration_ms_ = ms; }
|
||||||
@ -69,8 +73,8 @@ private:
|
|||||||
// VAD parameters - Higher threshold to avoid false triggers on filtered noise
|
// VAD parameters - Higher threshold to avoid false triggers on filtered noise
|
||||||
std::atomic<float> vad_rms_threshold_{0.02f}; // Was 0.01f
|
std::atomic<float> vad_rms_threshold_{0.02f}; // Was 0.01f
|
||||||
std::atomic<float> vad_peak_threshold_{0.08f}; // Was 0.04f
|
std::atomic<float> vad_peak_threshold_{0.08f}; // Was 0.04f
|
||||||
int silence_duration_ms_ = 700; // Wait 700ms of silence before cutting (was 400)
|
int silence_duration_ms_ = 400; // Wait 400ms of silence before cutting
|
||||||
int min_speech_duration_ms_ = 1000; // Minimum 1s speech to send (was 300)
|
int min_speech_duration_ms_ = 300; // Minimum speech to send
|
||||||
int max_speech_duration_ms_ = 25000; // 25s max before forced flush
|
int max_speech_duration_ms_ = 25000; // 25s max before forced flush
|
||||||
|
|
||||||
// Adaptive noise floor
|
// Adaptive noise floor
|
||||||
@ -79,7 +83,7 @@ private:
|
|||||||
|
|
||||||
// Hang time - wait before cutting to avoid mid-sentence cuts
|
// Hang time - wait before cutting to avoid mid-sentence cuts
|
||||||
int hang_frames_ = 0;
|
int hang_frames_ = 0;
|
||||||
int hang_frames_threshold_ = 35; // ~350ms tolerance for pauses (was 20)
|
int hang_frames_threshold_ = 20; // ~200ms tolerance for pauses
|
||||||
|
|
||||||
// Zero-crossing rate for speech vs noise discrimination
|
// Zero-crossing rate for speech vs noise discrimination
|
||||||
float last_zcr_ = 0.0f;
|
float last_zcr_ = 0.0f;
|
||||||
@ -90,6 +94,9 @@ private:
|
|||||||
std::atomic<float> current_rms_{0.0f};
|
std::atomic<float> current_rms_{0.0f};
|
||||||
std::atomic<float> current_peak_{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
|
// Noise reduction
|
||||||
std::unique_ptr<NoiseReducer> noise_reducer_;
|
std::unique_ptr<NoiseReducer> noise_reducer_;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include "../api/ClaudeClient.h"
|
#include "../api/ClaudeClient.h"
|
||||||
#include "../ui/TranslationUI.h"
|
#include "../ui/TranslationUI.h"
|
||||||
#include "../utils/Config.h"
|
#include "../utils/Config.h"
|
||||||
|
#include "../utils/SessionLogger.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@ -61,6 +62,10 @@ bool Pipeline::initialize() {
|
|||||||
// Create recordings directory if it doesn't exist
|
// Create recordings directory if it doesn't exist
|
||||||
std::filesystem::create_directories(config.getRecordingConfig().output_directory);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,10 +75,15 @@ bool Pipeline::start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
running_ = true;
|
running_ = true;
|
||||||
segment_id_ = 0;
|
|
||||||
|
|
||||||
// Start session logging
|
// Start session logging
|
||||||
session_logger_.startSession();
|
if (session_logger_) {
|
||||||
|
session_logger_->startSession();
|
||||||
|
session_logger_->setVadSettings(
|
||||||
|
ui_ ? ui_->getVadThreshold() : 0.02f,
|
||||||
|
ui_ ? ui_->getVadPeakThreshold() : 0.08f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Start background threads
|
// Start background threads
|
||||||
audio_thread_ = std::thread(&Pipeline::audioThread, this);
|
audio_thread_ = std::thread(&Pipeline::audioThread, this);
|
||||||
@ -132,7 +142,9 @@ void Pipeline::stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// End session logging
|
// End session logging
|
||||||
session_logger_.endSession();
|
if (session_logger_) {
|
||||||
|
session_logger_->endSession();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Pipeline::audioThread() {
|
void Pipeline::audioThread() {
|
||||||
@ -144,14 +156,26 @@ void Pipeline::audioThread() {
|
|||||||
// Add to full recording
|
// Add to full recording
|
||||||
full_recording_->addSamples(audio_data);
|
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;
|
AudioChunk chunk;
|
||||||
chunk.data = audio_data;
|
chunk.data = audio_data;
|
||||||
chunk.sample_rate = config.getAudioConfig().sample_rate;
|
chunk.sample_rate = config.getAudioConfig().sample_rate;
|
||||||
chunk.channels = config.getAudioConfig().channels;
|
chunk.channels = config.getAudioConfig().channels;
|
||||||
|
chunk.rms_level = rms;
|
||||||
|
chunk.peak_level = max_amp;
|
||||||
|
chunk.timestamp = std::chrono::system_clock::now();
|
||||||
|
|
||||||
float push_duration = static_cast<float>(audio_data.size()) / (chunk.sample_rate * chunk.channels);
|
|
||||||
std::cout << "[Queue] Pushing " << push_duration << "s chunk, queue size: " << audio_queue_.size() << std::endl;
|
|
||||||
audio_queue_.push(std::move(chunk));
|
audio_queue_.push(std::move(chunk));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -168,7 +192,6 @@ void Pipeline::audioThread() {
|
|||||||
|
|
||||||
void Pipeline::processingThread() {
|
void Pipeline::processingThread() {
|
||||||
auto& config = Config::getInstance();
|
auto& config = Config::getInstance();
|
||||||
int audio_segment_id = 0;
|
|
||||||
|
|
||||||
while (running_) {
|
while (running_) {
|
||||||
auto chunk_opt = audio_queue_.wait_and_pop();
|
auto chunk_opt = audio_queue_.wait_and_pop();
|
||||||
@ -178,44 +201,38 @@ void Pipeline::processingThread() {
|
|||||||
|
|
||||||
auto& chunk = chunk_opt.value();
|
auto& chunk = chunk_opt.value();
|
||||||
float duration = static_cast<float>(chunk.data.size()) / (chunk.sample_rate * chunk.channels);
|
float duration = static_cast<float>(chunk.data.size()) / (chunk.sample_rate * chunk.channels);
|
||||||
|
std::cout << "[Processing] Speech segment: " << duration << "s" << std::endl;
|
||||||
|
|
||||||
// Debug: log queue size to detect double-push
|
// Prepare segment data for logging
|
||||||
std::cout << "[Queue] Processing chunk, " << audio_queue_.size() << " remaining" << std::endl;
|
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 segment to session directory for debugging
|
// Save audio for this segment (also calculates hashes for duplicate detection)
|
||||||
audio_segment_id++;
|
if (session_logger_) {
|
||||||
if (session_logger_.isActive()) {
|
segment.audio_filename = session_logger_->saveSegmentAudio(
|
||||||
std::stringstream audio_path;
|
segment.id, chunk.data, chunk.sample_rate, chunk.channels,
|
||||||
audio_path << session_logger_.getSessionPath() << "/audio_"
|
segment.audio_hashes);
|
||||||
<< std::setfill('0') << std::setw(3) << audio_segment_id << ".ogg";
|
|
||||||
|
|
||||||
AudioBuffer segment_buffer(chunk.sample_rate, chunk.channels);
|
|
||||||
segment_buffer.addSamples(chunk.data);
|
|
||||||
if (segment_buffer.saveToOpus(audio_path.str())) {
|
|
||||||
std::cout << "[Session] Saved audio segment: " << audio_path.str() << std::endl;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate audio RMS for logging
|
|
||||||
float audio_rms = 0.0f;
|
|
||||||
if (!chunk.data.empty()) {
|
|
||||||
float sum_sq = 0.0f;
|
|
||||||
for (float s : chunk.data) sum_sq += s * s;
|
|
||||||
audio_rms = std::sqrt(sum_sq / chunk.data.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "[Processing] Speech segment: " << duration << "s (RMS=" << audio_rms << ")" << std::endl;
|
|
||||||
|
|
||||||
// Time Whisper
|
|
||||||
auto whisper_start = std::chrono::steady_clock::now();
|
|
||||||
|
|
||||||
// Build dynamic prompt with recent context
|
// Build dynamic prompt with recent context
|
||||||
std::string dynamic_prompt = buildDynamicPrompt();
|
std::string whisper_prompt = config.getWhisperConfig().prompt;
|
||||||
if (!recent_transcriptions_.empty()) {
|
if (session_logger_) {
|
||||||
std::cout << "[Context] Using " << recent_transcriptions_.size() << " previous segments" << std::endl;
|
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
|
// Transcribe with Whisper (measure latency)
|
||||||
|
auto whisper_start = std::chrono::steady_clock::now();
|
||||||
auto whisper_result = whisper_client_->transcribe(
|
auto whisper_result = whisper_client_->transcribe(
|
||||||
chunk.data,
|
chunk.data,
|
||||||
chunk.sample_rate,
|
chunk.sample_rate,
|
||||||
@ -223,17 +240,18 @@ void Pipeline::processingThread() {
|
|||||||
config.getWhisperConfig().model,
|
config.getWhisperConfig().model,
|
||||||
config.getWhisperConfig().language,
|
config.getWhisperConfig().language,
|
||||||
config.getWhisperConfig().temperature,
|
config.getWhisperConfig().temperature,
|
||||||
dynamic_prompt,
|
whisper_prompt,
|
||||||
config.getWhisperConfig().response_format
|
config.getWhisperConfig().response_format
|
||||||
);
|
);
|
||||||
|
|
||||||
auto whisper_end = std::chrono::steady_clock::now();
|
auto whisper_end = std::chrono::steady_clock::now();
|
||||||
int64_t whisper_latency = std::chrono::duration_cast<std::chrono::milliseconds>(
|
segment.whisper_latency_ms = std::chrono::duration<float, std::milli>(whisper_end - whisper_start).count();
|
||||||
whisper_end - whisper_start).count();
|
|
||||||
|
|
||||||
if (!whisper_result.has_value()) {
|
if (!whisper_result.has_value()) {
|
||||||
std::cerr << "Whisper transcription failed" << std::endl;
|
std::cerr << "Whisper transcription failed" << std::endl;
|
||||||
session_logger_.logFilteredSegment("", "whisper_failed", duration, audio_rms);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,7 +263,10 @@ void Pipeline::processingThread() {
|
|||||||
size_t end = text.find_last_not_of(" \t\n\r");
|
size_t end = text.find_last_not_of(" \t\n\r");
|
||||||
if (start == std::string::npos) {
|
if (start == std::string::npos) {
|
||||||
std::cout << "[Skip] Empty transcription" << std::endl;
|
std::cout << "[Skip] Empty transcription" << std::endl;
|
||||||
session_logger_.logFilteredSegment("", "empty", duration, audio_rms);
|
segment.was_filtered = true;
|
||||||
|
segment.filter_reason = "empty";
|
||||||
|
segment.end_time = std::chrono::system_clock::now();
|
||||||
|
if (session_logger_) session_logger_->logSegment(segment);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
text = text.substr(start, end - start + 1);
|
text = text.substr(start, end - start + 1);
|
||||||
@ -318,47 +339,37 @@ void Pipeline::processingThread() {
|
|||||||
|
|
||||||
if (is_garbage) {
|
if (is_garbage) {
|
||||||
std::cout << "[Skip] Filtered: " << text << std::endl;
|
std::cout << "[Skip] Filtered: " << text << std::endl;
|
||||||
session_logger_.logFilteredSegment(text, "hallucination", duration, audio_rms);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deduplication: skip if exact same as last transcription
|
|
||||||
if (text == last_transcription_) {
|
|
||||||
std::cout << "[Skip] Duplicate: " << text << std::endl;
|
|
||||||
session_logger_.logFilteredSegment(text, "duplicate", duration, audio_rms);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
last_transcription_ = text;
|
|
||||||
|
|
||||||
// Update dynamic context for next Whisper call
|
|
||||||
recent_transcriptions_.push_back(text);
|
|
||||||
if (recent_transcriptions_.size() > MAX_CONTEXT_SEGMENTS) {
|
|
||||||
recent_transcriptions_.erase(recent_transcriptions_.begin());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track audio cost
|
// Track audio cost
|
||||||
if (ui_) {
|
if (ui_) {
|
||||||
ui_->addAudioCost(duration);
|
ui_->addAudioCost(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time Claude
|
// Translate with Claude (measure latency)
|
||||||
auto claude_start = std::chrono::steady_clock::now();
|
auto claude_start = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
// Translate with Claude
|
|
||||||
auto claude_result = claude_client_->translate(
|
auto claude_result = claude_client_->translate(
|
||||||
text,
|
text,
|
||||||
config.getClaudeConfig().system_prompt,
|
config.getClaudeConfig().system_prompt,
|
||||||
config.getClaudeConfig().max_tokens,
|
config.getClaudeConfig().max_tokens,
|
||||||
config.getClaudeConfig().temperature
|
config.getClaudeConfig().temperature
|
||||||
);
|
);
|
||||||
|
|
||||||
auto claude_end = std::chrono::steady_clock::now();
|
auto claude_end = std::chrono::steady_clock::now();
|
||||||
int64_t claude_latency = std::chrono::duration_cast<std::chrono::milliseconds>(
|
segment.claude_latency_ms = std::chrono::duration<float, std::milli>(claude_end - claude_start).count();
|
||||||
claude_end - claude_start).count();
|
|
||||||
|
|
||||||
if (!claude_result.has_value()) {
|
if (!claude_result.has_value()) {
|
||||||
std::cerr << "Claude translation failed" << std::endl;
|
std::cerr << "Claude translation failed" << std::endl;
|
||||||
session_logger_.logFilteredSegment(text, "claude_failed", duration, audio_rms);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,6 +378,14 @@ void Pipeline::processingThread() {
|
|||||||
ui_->addClaudeCost();
|
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
|
// Simple accumulation
|
||||||
if (!accumulated_chinese_.empty()) {
|
if (!accumulated_chinese_.empty()) {
|
||||||
accumulated_chinese_ += " ";
|
accumulated_chinese_ += " ";
|
||||||
@ -382,24 +401,8 @@ void Pipeline::processingThread() {
|
|||||||
ui_->setAccumulatedText(accumulated_chinese_, accumulated_french_);
|
ui_->setAccumulatedText(accumulated_chinese_, accumulated_french_);
|
||||||
ui_->addTranslation(text, claude_result->text);
|
ui_->addTranslation(text, claude_result->text);
|
||||||
|
|
||||||
// Log successful segment
|
|
||||||
segment_id_++;
|
|
||||||
SegmentLog seg;
|
|
||||||
seg.id = segment_id_;
|
|
||||||
seg.chinese = text;
|
|
||||||
seg.french = claude_result->text;
|
|
||||||
seg.audio_duration_sec = duration;
|
|
||||||
seg.audio_rms = audio_rms;
|
|
||||||
seg.whisper_latency_ms = whisper_latency;
|
|
||||||
seg.claude_latency_ms = claude_latency;
|
|
||||||
seg.was_filtered = false;
|
|
||||||
seg.filter_reason = "";
|
|
||||||
seg.timestamp = ""; // Will be set by logger
|
|
||||||
session_logger_.logSegment(seg);
|
|
||||||
|
|
||||||
std::cout << "CN: " << text << std::endl;
|
std::cout << "CN: " << text << std::endl;
|
||||||
std::cout << "FR: " << claude_result->text << std::endl;
|
std::cout << "FR: " << claude_result->text << std::endl;
|
||||||
std::cout << "[Latency] Whisper: " << whisper_latency << "ms, Claude: " << claude_latency << "ms" << std::endl;
|
|
||||||
std::cout << "---" << std::endl;
|
std::cout << "---" << std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -407,12 +410,13 @@ void Pipeline::processingThread() {
|
|||||||
void Pipeline::update() {
|
void Pipeline::update() {
|
||||||
if (!ui_) return;
|
if (!ui_) return;
|
||||||
|
|
||||||
// Sync VAD thresholds from UI to AudioCapture
|
// Sync VAD thresholds and input gain from UI to AudioCapture
|
||||||
if (audio_capture_) {
|
if (audio_capture_) {
|
||||||
audio_capture_->setVadThresholds(
|
audio_capture_->setVadThresholds(
|
||||||
ui_->getVadThreshold(),
|
ui_->getVadThreshold(),
|
||||||
ui_->getVadPeakThreshold()
|
ui_->getVadPeakThreshold()
|
||||||
);
|
);
|
||||||
|
audio_capture_->setInputGain(ui_->getInputGain());
|
||||||
|
|
||||||
// Update UI with audio levels
|
// Update UI with audio levels
|
||||||
ui_->setCurrentRMS(audio_capture_->getCurrentRMS());
|
ui_->setCurrentRMS(audio_capture_->getCurrentRMS());
|
||||||
@ -448,34 +452,10 @@ bool Pipeline::shouldClose() const {
|
|||||||
void Pipeline::clearAccumulated() {
|
void Pipeline::clearAccumulated() {
|
||||||
accumulated_chinese_.clear();
|
accumulated_chinese_.clear();
|
||||||
accumulated_french_.clear();
|
accumulated_french_.clear();
|
||||||
recent_transcriptions_.clear();
|
|
||||||
last_transcription_.clear();
|
|
||||||
if (ui_) {
|
if (ui_) {
|
||||||
ui_->setAccumulatedText("", "");
|
ui_->setAccumulatedText("", "");
|
||||||
}
|
}
|
||||||
std::cout << "[Pipeline] Cleared accumulated text and context" << std::endl;
|
std::cout << "[Pipeline] Cleared accumulated text" << std::endl;
|
||||||
}
|
|
||||||
|
|
||||||
std::string Pipeline::buildDynamicPrompt() const {
|
|
||||||
auto& config = Config::getInstance();
|
|
||||||
std::string base_prompt = config.getWhisperConfig().prompt;
|
|
||||||
|
|
||||||
// If no recent transcriptions, just return base prompt
|
|
||||||
if (recent_transcriptions_.empty()) {
|
|
||||||
return base_prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build context from recent transcriptions
|
|
||||||
std::stringstream context;
|
|
||||||
context << base_prompt;
|
|
||||||
context << "\n\nContexte des phrases précédentes: ";
|
|
||||||
|
|
||||||
for (size_t i = 0; i < recent_transcriptions_.size(); ++i) {
|
|
||||||
if (i > 0) context << " ";
|
|
||||||
context << recent_transcriptions_[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.str();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace secondvoice
|
} // namespace secondvoice
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <chrono>
|
||||||
#include "../utils/ThreadSafeQueue.h"
|
#include "../utils/ThreadSafeQueue.h"
|
||||||
#include "../utils/SessionLogger.h"
|
|
||||||
|
|
||||||
namespace secondvoice {
|
namespace secondvoice {
|
||||||
|
|
||||||
@ -15,11 +15,15 @@ class WhisperClient;
|
|||||||
class ClaudeClient;
|
class ClaudeClient;
|
||||||
class TranslationUI;
|
class TranslationUI;
|
||||||
class AudioBuffer;
|
class AudioBuffer;
|
||||||
|
class SessionLogger;
|
||||||
|
|
||||||
struct AudioChunk {
|
struct AudioChunk {
|
||||||
std::vector<float> data;
|
std::vector<float> data;
|
||||||
int sample_rate;
|
int sample_rate;
|
||||||
int channels;
|
int channels;
|
||||||
|
float rms_level;
|
||||||
|
float peak_level;
|
||||||
|
std::chrono::system_clock::time_point timestamp;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Pipeline {
|
class Pipeline {
|
||||||
@ -49,6 +53,7 @@ private:
|
|||||||
std::unique_ptr<ClaudeClient> claude_client_;
|
std::unique_ptr<ClaudeClient> claude_client_;
|
||||||
std::unique_ptr<TranslationUI> ui_;
|
std::unique_ptr<TranslationUI> ui_;
|
||||||
std::unique_ptr<AudioBuffer> full_recording_;
|
std::unique_ptr<AudioBuffer> full_recording_;
|
||||||
|
std::unique_ptr<SessionLogger> session_logger_;
|
||||||
|
|
||||||
ThreadSafeQueue<AudioChunk> audio_queue_;
|
ThreadSafeQueue<AudioChunk> audio_queue_;
|
||||||
|
|
||||||
@ -61,20 +66,6 @@ private:
|
|||||||
// Simple accumulation
|
// Simple accumulation
|
||||||
std::string accumulated_chinese_;
|
std::string accumulated_chinese_;
|
||||||
std::string accumulated_french_;
|
std::string accumulated_french_;
|
||||||
|
|
||||||
// Dynamic context for Whisper (last N transcriptions)
|
|
||||||
std::vector<std::string> recent_transcriptions_;
|
|
||||||
static constexpr size_t MAX_CONTEXT_SEGMENTS = 3;
|
|
||||||
|
|
||||||
// Deduplication: skip if same as last transcription
|
|
||||||
std::string last_transcription_;
|
|
||||||
|
|
||||||
// Build dynamic prompt with recent context
|
|
||||||
std::string buildDynamicPrompt() const;
|
|
||||||
|
|
||||||
// Session logging
|
|
||||||
SessionLogger session_logger_;
|
|
||||||
int segment_id_ = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace secondvoice
|
} // namespace secondvoice
|
||||||
|
|||||||
@ -400,6 +400,17 @@ void TranslationUI::renderAudioPanel() {
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
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
|
// VAD Threshold sliders
|
||||||
ImGui::Text("VAD Settings");
|
ImGui::Text("VAD Settings");
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|||||||
@ -44,6 +44,9 @@ public:
|
|||||||
float getVadThreshold() const { return vad_threshold_; }
|
float getVadThreshold() const { return vad_threshold_; }
|
||||||
float getVadPeakThreshold() const { return vad_peak_threshold_; }
|
float getVadPeakThreshold() const { return vad_peak_threshold_; }
|
||||||
|
|
||||||
|
// Input gain (amplifier)
|
||||||
|
float getInputGain() const { return input_gain_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void renderAccumulated();
|
void renderAccumulated();
|
||||||
void renderTranslations();
|
void renderTranslations();
|
||||||
@ -71,6 +74,7 @@ private:
|
|||||||
float current_peak_ = 0.0f;
|
float current_peak_ = 0.0f;
|
||||||
float vad_threshold_ = 0.02f; // 2x higher to avoid false triggers
|
float vad_threshold_ = 0.02f; // 2x higher to avoid false triggers
|
||||||
float vad_peak_threshold_ = 0.08f; // 2x higher
|
float vad_peak_threshold_ = 0.08f; // 2x higher
|
||||||
|
float input_gain_ = 1.0f; // Input amplifier (1.0 = no change)
|
||||||
|
|
||||||
// Cost tracking
|
// Cost tracking
|
||||||
float total_audio_seconds_ = 0.0f;
|
float total_audio_seconds_ = 0.0f;
|
||||||
|
|||||||
@ -1,196 +1,412 @@
|
|||||||
#include "SessionLogger.h"
|
#include "SessionLogger.h"
|
||||||
#include <nlohmann/json.hpp>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <sstream>
|
#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 {
|
namespace secondvoice {
|
||||||
|
|
||||||
using json = nlohmann::json;
|
SessionLogger::SessionLogger() {
|
||||||
|
metadata_ = {};
|
||||||
SessionLogger::SessionLogger() = default;
|
std::srand(static_cast<unsigned int>(std::time(nullptr))); // For OGG stream IDs
|
||||||
|
}
|
||||||
|
|
||||||
SessionLogger::~SessionLogger() {
|
SessionLogger::~SessionLogger() {
|
||||||
if (is_active_) {
|
if (session_active_) {
|
||||||
endSession();
|
endSession();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string SessionLogger::getCurrentTimestamp() const {
|
bool SessionLogger::startSession() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
// Generate session ID from timestamp
|
||||||
auto now = std::chrono::system_clock::now();
|
auto now = std::chrono::system_clock::now();
|
||||||
auto time_t = std::chrono::system_clock::to_time_t(now);
|
auto time_t = std::chrono::system_clock::to_time_t(now);
|
||||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
||||||
now.time_since_epoch()) % 1000;
|
|
||||||
|
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H%M%S");
|
ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H%M%S");
|
||||||
return ss.str();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionLogger::startSession() {
|
// Initialize metadata
|
||||||
if (is_active_) {
|
metadata_.total_segments = 0;
|
||||||
endSession();
|
metadata_.filtered_segments = 0;
|
||||||
}
|
metadata_.total_audio_seconds = 0.0f;
|
||||||
|
metadata_.total_cost_estimate = 0.0f;
|
||||||
|
|
||||||
session_start_time_ = getCurrentTimestamp();
|
next_segment_id_ = 1;
|
||||||
session_path_ = "./sessions/" + session_start_time_;
|
|
||||||
|
|
||||||
// Create directories
|
|
||||||
std::filesystem::create_directories(session_path_ + "/segments");
|
|
||||||
|
|
||||||
is_active_ = true;
|
|
||||||
segment_count_ = 0;
|
|
||||||
filtered_count_ = 0;
|
|
||||||
total_audio_sec_ = 0.0f;
|
|
||||||
total_whisper_ms_ = 0;
|
|
||||||
total_claude_ms_ = 0;
|
|
||||||
segments_.clear();
|
segments_.clear();
|
||||||
|
session_active_ = true;
|
||||||
|
|
||||||
std::cout << "[Session] Started: " << session_path_ << std::endl;
|
std::cout << "[SessionLogger] Started session: " << metadata_.session_id << std::endl;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionLogger::endSession() {
|
void SessionLogger::endSession() {
|
||||||
if (!is_active_) return;
|
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();
|
writeSessionJson();
|
||||||
is_active_ = false;
|
|
||||||
|
|
||||||
std::cout << "[Session] Ended: " << segment_count_ << " segments, "
|
session_active_ = false;
|
||||||
<< filtered_count_ << " filtered, "
|
std::cout << "[SessionLogger] Ended session: " << metadata_.session_id
|
||||||
<< total_audio_sec_ << "s audio" << std::endl;
|
<< " (" << metadata_.total_segments << " segments)" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionLogger::logSegment(const SegmentLog& segment) {
|
void SessionLogger::logSegment(const SegmentData& segment) {
|
||||||
if (!is_active_) return;
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
// Update counters
|
if (!session_active_) return;
|
||||||
segment_count_++;
|
|
||||||
total_audio_sec_ += segment.audio_duration_sec;
|
|
||||||
total_whisper_ms_ += segment.whisper_latency_ms;
|
|
||||||
total_claude_ms_ += segment.claude_latency_ms;
|
|
||||||
|
|
||||||
// Store segment
|
|
||||||
segments_.push_back(segment);
|
segments_.push_back(segment);
|
||||||
|
next_segment_id_++; // Increment for next segment
|
||||||
|
|
||||||
// Write individual segment JSON
|
if (segment.was_filtered) {
|
||||||
std::stringstream filename;
|
metadata_.filtered_segments++;
|
||||||
filename << session_path_ << "/segments/"
|
}
|
||||||
<< std::setfill('0') << std::setw(3) << segment.id << ".json";
|
metadata_.total_audio_seconds += segment.duration_seconds;
|
||||||
|
|
||||||
json j;
|
// Estimate cost: Whisper $0.006/min, Claude ~$0.001/call
|
||||||
j["id"] = segment.id;
|
float whisper_cost = (segment.duration_seconds / 60.0f) * 0.006f;
|
||||||
j["chinese"] = segment.chinese;
|
float claude_cost = segment.was_filtered ? 0.0f : 0.001f;
|
||||||
j["french"] = segment.french;
|
metadata_.total_cost_estimate += whisper_cost + claude_cost;
|
||||||
j["audio_duration_sec"] = segment.audio_duration_sec;
|
|
||||||
j["audio_rms"] = segment.audio_rms;
|
|
||||||
j["whisper_latency_ms"] = segment.whisper_latency_ms;
|
|
||||||
j["claude_latency_ms"] = segment.claude_latency_ms;
|
|
||||||
j["was_filtered"] = segment.was_filtered;
|
|
||||||
j["filter_reason"] = segment.filter_reason;
|
|
||||||
j["timestamp"] = segment.timestamp;
|
|
||||||
|
|
||||||
std::ofstream file(filename.str());
|
writeSegmentJson(segment);
|
||||||
if (file.is_open()) {
|
|
||||||
file << j.dump(2);
|
|
||||||
file.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "[Session] Logged segment #" << segment.id
|
std::string SessionLogger::saveSegmentAudio(int segment_id, const std::vector<float>& audio_data,
|
||||||
<< " (" << segment.audio_duration_sec << "s)" << std::endl;
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionLogger::logFilteredSegment(const std::string& chinese, const std::string& reason,
|
std::cout << "[SessionLogger] Audio hashes (" << num_seconds << "s): ";
|
||||||
float audio_duration, float audio_rms) {
|
for (const auto& h : out_hashes) {
|
||||||
if (!is_active_) return;
|
std::cout << h.substr(0, 8) << " "; // Print first 8 chars for brevity
|
||||||
|
|
||||||
filtered_count_++;
|
|
||||||
total_audio_sec_ += audio_duration;
|
|
||||||
|
|
||||||
// Log filtered segment with special marker
|
|
||||||
SegmentLog seg;
|
|
||||||
seg.id = segment_count_ + filtered_count_;
|
|
||||||
seg.chinese = chinese;
|
|
||||||
seg.french = "[FILTERED]";
|
|
||||||
seg.audio_duration_sec = audio_duration;
|
|
||||||
seg.audio_rms = audio_rms;
|
|
||||||
seg.whisper_latency_ms = 0;
|
|
||||||
seg.claude_latency_ms = 0;
|
|
||||||
seg.was_filtered = true;
|
|
||||||
seg.filter_reason = reason;
|
|
||||||
seg.timestamp = getCurrentTimestamp();
|
|
||||||
|
|
||||||
segments_.push_back(seg);
|
|
||||||
|
|
||||||
// Write filtered segment JSON
|
|
||||||
std::stringstream filename;
|
|
||||||
filename << session_path_ << "/segments/"
|
|
||||||
<< std::setfill('0') << std::setw(3) << seg.id << "_filtered.json";
|
|
||||||
|
|
||||||
json j;
|
|
||||||
j["id"] = seg.id;
|
|
||||||
j["chinese"] = seg.chinese;
|
|
||||||
j["filter_reason"] = reason;
|
|
||||||
j["audio_duration_sec"] = audio_duration;
|
|
||||||
j["audio_rms"] = audio_rms;
|
|
||||||
j["timestamp"] = seg.timestamp;
|
|
||||||
|
|
||||||
std::ofstream file(filename.str());
|
|
||||||
if (file.is_open()) {
|
|
||||||
file << j.dump(2);
|
|
||||||
file.close();
|
|
||||||
}
|
}
|
||||||
|
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() {
|
void SessionLogger::writeSessionJson() {
|
||||||
json session;
|
|
||||||
session["start_time"] = session_start_time_;
|
|
||||||
session["end_time"] = getCurrentTimestamp();
|
|
||||||
session["total_segments"] = segment_count_;
|
|
||||||
session["filtered_segments"] = filtered_count_;
|
|
||||||
session["total_audio_seconds"] = total_audio_sec_;
|
|
||||||
session["avg_whisper_latency_ms"] = segment_count_ > 0 ?
|
|
||||||
total_whisper_ms_ / segment_count_ : 0;
|
|
||||||
session["avg_claude_latency_ms"] = segment_count_ > 0 ?
|
|
||||||
total_claude_ms_ / segment_count_ : 0;
|
|
||||||
|
|
||||||
// Summary of all segments
|
|
||||||
json segments_summary = json::array();
|
|
||||||
for (const auto& seg : segments_) {
|
|
||||||
json s;
|
|
||||||
s["id"] = seg.id;
|
|
||||||
s["chinese"] = seg.chinese;
|
|
||||||
s["french"] = seg.french;
|
|
||||||
s["duration"] = seg.audio_duration_sec;
|
|
||||||
s["filtered"] = seg.was_filtered;
|
|
||||||
if (seg.was_filtered) {
|
|
||||||
s["filter_reason"] = seg.filter_reason;
|
|
||||||
}
|
|
||||||
segments_summary.push_back(s);
|
|
||||||
}
|
|
||||||
session["segments"] = segments_summary;
|
|
||||||
|
|
||||||
std::string filepath = session_path_ + "/session.json";
|
std::string filepath = session_path_ + "/session.json";
|
||||||
std::ofstream file(filepath);
|
std::ofstream file(filepath, std::ios::out | std::ios::binary);
|
||||||
if (file.is_open()) {
|
if (!file.is_open()) {
|
||||||
file << session.dump(2);
|
std::cerr << "[SessionLogger] Failed to write session.json" << std::endl;
|
||||||
file.close();
|
return;
|
||||||
std::cout << "[Session] Wrote " << filepath << std::endl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also write plain text transcript
|
// Write UTF-8 BOM
|
||||||
std::string transcript_path = session_path_ + "/transcript.txt";
|
file << "\xEF\xBB\xBF";
|
||||||
std::ofstream transcript(transcript_path);
|
|
||||||
if (transcript.is_open()) {
|
// Manual JSON construction (to avoid extra dependencies)
|
||||||
transcript << "=== SecondVoice Session " << session_start_time_ << " ===\n\n";
|
file << "{\n";
|
||||||
for (const auto& seg : segments_) {
|
file << " \"session_id\": \"" << metadata_.session_id << "\",\n";
|
||||||
if (!seg.was_filtered) {
|
file << " \"start_time\": \"" << formatTimestamp(metadata_.start_time) << "\",\n";
|
||||||
transcript << "CN: " << seg.chinese << "\n";
|
file << " \"end_time\": \"" << formatTimestamp(metadata_.end_time) << "\",\n";
|
||||||
transcript << "FR: " << seg.french << "\n\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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
transcript.close();
|
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
|
} // namespace secondvoice
|
||||||
|
|||||||
@ -4,20 +4,54 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
namespace secondvoice {
|
namespace secondvoice {
|
||||||
|
|
||||||
struct SegmentLog {
|
struct SegmentData {
|
||||||
int id;
|
int id;
|
||||||
std::string chinese;
|
std::string chinese;
|
||||||
std::string french;
|
std::string french;
|
||||||
float audio_duration_sec;
|
|
||||||
float audio_rms;
|
// Audio metadata
|
||||||
int64_t whisper_latency_ms;
|
float duration_seconds;
|
||||||
int64_t claude_latency_ms;
|
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;
|
bool was_filtered;
|
||||||
std::string filter_reason;
|
std::string filter_reason;
|
||||||
std::string timestamp;
|
|
||||||
|
// 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 {
|
class SessionLogger {
|
||||||
@ -25,39 +59,46 @@ public:
|
|||||||
SessionLogger();
|
SessionLogger();
|
||||||
~SessionLogger();
|
~SessionLogger();
|
||||||
|
|
||||||
// Start a new session (creates directory)
|
// Start a new session - creates directory structure
|
||||||
void startSession();
|
bool startSession();
|
||||||
|
|
||||||
// End session (writes session.json summary)
|
// End session - writes session.json
|
||||||
void endSession();
|
void endSession();
|
||||||
|
|
||||||
// Log a segment
|
// Log a segment (called after transcription+translation)
|
||||||
void logSegment(const SegmentLog& segment);
|
void logSegment(const SegmentData& segment);
|
||||||
|
|
||||||
// Log a filtered/skipped segment
|
// Save audio data for a segment (returns filename)
|
||||||
void logFilteredSegment(const std::string& chinese, const std::string& reason,
|
// Also calculates audio_hashes (one hash per second)
|
||||||
float audio_duration, float audio_rms);
|
std::string saveSegmentAudio(int segment_id, const std::vector<float>& audio_data,
|
||||||
|
int sample_rate, int channels,
|
||||||
|
std::vector<std::string>& out_hashes);
|
||||||
|
|
||||||
// Get current session path
|
// Update session metadata
|
||||||
std::string getSessionPath() const { return session_path_; }
|
void setVadSettings(float rms_thresh, float peak_thresh);
|
||||||
|
void setModels(const std::string& whisper_model, const std::string& claude_model);
|
||||||
|
|
||||||
// Check if session is active
|
// Getters
|
||||||
bool isActive() const { return is_active_; }
|
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:
|
private:
|
||||||
std::string getCurrentTimestamp() const;
|
|
||||||
void writeSessionJson();
|
void writeSessionJson();
|
||||||
|
void writeSegmentJson(const SegmentData& segment);
|
||||||
|
std::string formatTimestamp(const std::chrono::system_clock::time_point& tp) const;
|
||||||
|
|
||||||
bool is_active_ = false;
|
|
||||||
std::string session_path_;
|
std::string session_path_;
|
||||||
std::string session_start_time_;
|
std::string segments_path_;
|
||||||
int segment_count_ = 0;
|
SessionMetadata metadata_;
|
||||||
int filtered_count_ = 0;
|
std::vector<SegmentData> segments_;
|
||||||
float total_audio_sec_ = 0.0f;
|
|
||||||
int total_whisper_ms_ = 0;
|
|
||||||
int total_claude_ms_ = 0;
|
|
||||||
|
|
||||||
std::vector<SegmentLog> segments_;
|
int next_segment_id_ = 1;
|
||||||
|
bool session_active_ = false;
|
||||||
|
|
||||||
|
mutable std::mutex mutex_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace secondvoice
|
} // 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