Add PT-BR Talker POC metadata, scripts and evaluation outputs
Browse files- .gitattributes +5 -0
- README.md +74 -0
- SHA256SUMS.txt +4 -0
- data/sft_text_ptbr_curriculum_100k.parquet +3 -0
- eval/aula.wav +3 -0
- eval/hotel.wav +3 -0
- eval/mercado.wav +3 -0
- eval/onibus.wav +3 -0
- eval/padaria.wav +3 -0
- eval/ptbr_talker_eval.csv +6 -0
- eval/ptbr_talker_eval.jsonl +5 -0
- eval/summary.json +9 -0
- file_manifest.json +82 -0
- metadata.json +14 -0
- tools/build_ptbr_conversation_curriculum.py +354 -0
- tools/eval_minimind_talker_ptbr.py +217 -0
- tools/run_talker_finetune_phases.py +356 -0
- tools/validate_talker_finetune_phases.py +264 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
eval/aula.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
eval/hotel.wav filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
eval/mercado.wav filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
eval/onibus.wav filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
eval/padaria.wav filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: other
|
| 3 |
+
language:
|
| 4 |
+
- pt
|
| 5 |
+
tags:
|
| 6 |
+
- minimind-o
|
| 7 |
+
- portuguese
|
| 8 |
+
- pt-br
|
| 9 |
+
- text-to-audio
|
| 10 |
+
- mimi
|
| 11 |
+
- experimental
|
| 12 |
+
private: true
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# MiniMind-O PT-BR Talker Curriculum 100k POC
|
| 16 |
+
|
| 17 |
+
Experimental PT-BR fine-tuning bundle metadata for MiniMind-O text/audio work.
|
| 18 |
+
|
| 19 |
+
**Important:** the large `.pth` checkpoints were prepared locally but could not be uploaded to this private repository because the Hugging Face account hit its private LFS storage limit. This repository therefore contains the reproducibility scripts, evaluation outputs, metadata, file manifest, and checksums. The large files are available locally in the prepared export folder.
|
| 20 |
+
|
| 21 |
+
## Prepared Local Artifacts
|
| 22 |
+
|
| 23 |
+
Local export folder:
|
| 24 |
+
|
| 25 |
+
```text
|
| 26 |
+
hf_exports/minimind-o-ptbr-talker-curriculum-100k-poc/
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
Prepared checkpoints:
|
| 30 |
+
|
| 31 |
+
```text
|
| 32 |
+
checkpoints/sft_ptbr_curriculum_100k_textfix_768.pth
|
| 33 |
+
checkpoints/sft_ptbr_curriculum_100k_t2a_repair_768.pth
|
| 34 |
+
checkpoints/sft_ptbr_curriculum_100k_mixed_768.pth
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Prepared data:
|
| 38 |
+
|
| 39 |
+
```text
|
| 40 |
+
data/ptbr_curriculum_100k.jsonl
|
| 41 |
+
data/sft_text_ptbr_curriculum_100k.parquet
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
See `SHA256SUMS.txt` and `file_manifest.json` for sizes and checksums.
|
| 45 |
+
|
| 46 |
+
## Evaluation Summary
|
| 47 |
+
|
| 48 |
+
Final evaluation after textfix + T2A repair + mixed SFT:
|
| 49 |
+
|
| 50 |
+
```json
|
| 51 |
+
{
|
| 52 |
+
"rows": 5,
|
| 53 |
+
"audio_generated": 5,
|
| 54 |
+
"transcribed": 5,
|
| 55 |
+
"looks_portuguese": 3,
|
| 56 |
+
"basic_pass": 5
|
| 57 |
+
}
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
Manual reading: audio generation works, but PT-BR/relevance is still not production-ready. One sample still mixes English/alucinated content, and several transcriptions remain rough.
|
| 61 |
+
|
| 62 |
+
## Training Flow
|
| 63 |
+
|
| 64 |
+
1. Build PT-BR curriculum 100k.
|
| 65 |
+
2. Train textfix from `sft_omni_ptbr_talker`.
|
| 66 |
+
3. Repair T2A on the 6h PT-BR T2A parquet.
|
| 67 |
+
4. Train mixed text + T2A.
|
| 68 |
+
5. Evaluate with generated WAVs and Whisper Medium.
|
| 69 |
+
|
| 70 |
+
Relevant scripts are included under `tools/`.
|
| 71 |
+
|
| 72 |
+
## Caveat
|
| 73 |
+
|
| 74 |
+
These `.pth` files are MiniMind-O native checkpoints, not Hugging Face Transformers model directories. They need the MiniMind-O codebase and matching config/model files to load.
|
SHA256SUMS.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
366ddb7ec27897f0e19b1a0bc9b96eb0370167864f218158c19958969bd1a8b4 checkpoints/sft_ptbr_curriculum_100k_mixed_768.pth
|
| 2 |
+
f44c4b22dba654d17248330a481f2301ef3366f99fa0c46b0cd025b405714791 checkpoints/sft_ptbr_curriculum_100k_t2a_repair_768.pth
|
| 3 |
+
06edc9718b87502ad67ab5c5e9aaba6d72cd453de460e3cd454e6e80f247c7fe checkpoints/sft_ptbr_curriculum_100k_textfix_768.pth
|
| 4 |
+
60cfc078f9d79fb67b788d7e178be005ce57d7ed6ad4d3670c6d49619c7104d8 data/sft_text_ptbr_curriculum_100k.parquet
|
data/sft_text_ptbr_curriculum_100k.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:60cfc078f9d79fb67b788d7e178be005ce57d7ed6ad4d3670c6d49619c7104d8
|
| 3 |
+
size 6016108
|
eval/aula.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fadafff3a424db89b91a952044b5e57604bf7409887bbbd2e597fd38cd5dc877
|
| 3 |
+
size 430124
|
eval/hotel.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:57be946de8da9bd60f95b0bd46e36c7a1d61ba770a0b51c938fcdcbd379a9ec9
|
| 3 |
+
size 145964
|
eval/mercado.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:02c72add1902de3a1f306c1ca3343b10a063b941d170ed8a427ac7b3520d664f
|
| 3 |
+
size 299564
|
eval/onibus.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:084a7f8d35dc6f0a98f4c0af834f3b8f5e51b00a3b5aaffec44785f75e8ed550
|
| 3 |
+
size 145964
|
eval/padaria.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0726d7600523a94c92000fcd4861031f0901ce47119ab9554b24f319b587cd72
|
| 3 |
+
size 138284
|
eval/ptbr_talker_eval.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
answer_text,audio_frames,generated_audio,id,looks_portuguese,non_empty_answer_text,non_empty_transcript,pass_basic,prompt,target_style,transcript,valid_mimi_frames,wav_path,whisper_error
|
| 2 |
+
Você está está uma padaria no Brasil.,36,True,padaria,True,True,True,True,Você está em uma padaria no Brasil. Cumprimente o atendente e peça um café com leite.,"português brasileiro, curto, conversa cotidiana","Logo em Steyr, está aí uma padada no prosaile...",36,eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k/padaria.wav,
|
| 3 |
+
"Você está no mercado na quelque aconoros, o que vocês esta espação.",78,True,mercado,True,True,True,True,Você está no mercado. Pergunte onde ficam as frutas e agradeça de forma natural.,"português brasileiro, educado, sem inglês",Vou listar o meu chegado na quadro com búrbios assistentes. Revista a cipação.,78,eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k/mercado.wav,
|
| 4 |
+
Peça ajuda para encontrar a recepção.,38,True,hotel,False,True,True,True,Você chegou em um hotel. Peça ajuda para encontrar a recepção.,"português brasileiro, simples, uma ou duas frases",Eu ajudo a para encontrar esse de passar,38,eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k/hotel.wav,
|
| 5 |
+
"Paraumum aluno o que significa praticar uma língua todos os dias is indeed a debated topic. In theory, it refers to the principal ingredient, which is a type of algae os, commonly found in the savanna, which is a herbaceous herb. There is no evidence to support this conclusion, as the principal ingredient, which is a",112,True,aula,False,True,True,True,Explique para um aluno o que significa praticar uma língua todos os dias.,"português brasileiro, didático, resposta curta","Pera do mundo, o que se caparicar? Como alhar engloar todos os alhas enteidedadas? E inte...",112,eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k/aula.wav,
|
| 6 |
+
Passaê está no pondto de ônibus.,38,True,onibus,True,True,True,True,Você está no ponto de ônibus. Pergunte se aquele ônibus passa pelo centro.,"português brasileiro, fala cotidiana","Faça a pista, um ponto de Salibus.",38,eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k/onibus.wav,
|
eval/ptbr_talker_eval.jsonl
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"id": "padaria", "prompt": "Você está em uma padaria no Brasil. Cumprimente o atendente e peça um café com leite.", "target_style": "português brasileiro, curto, conversa cotidiana", "answer_text": "Você está está uma padaria no Brasil.", "audio_frames": 36, "valid_mimi_frames": 36, "wav_path": "eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k/padaria.wav", "transcript": "Logo em Steyr, está aí uma padada no prosaile...", "whisper_error": "", "generated_audio": true, "non_empty_transcript": true, "looks_portuguese": true, "non_empty_answer_text": true, "pass_basic": true}
|
| 2 |
+
{"id": "mercado", "prompt": "Você está no mercado. Pergunte onde ficam as frutas e agradeça de forma natural.", "target_style": "português brasileiro, educado, sem inglês", "answer_text": "Você está no mercado na quelque aconoros, o que vocês esta espação.", "audio_frames": 78, "valid_mimi_frames": 78, "wav_path": "eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k/mercado.wav", "transcript": "Vou listar o meu chegado na quadro com búrbios assistentes. Revista a cipação.", "whisper_error": "", "generated_audio": true, "non_empty_transcript": true, "looks_portuguese": true, "non_empty_answer_text": true, "pass_basic": true}
|
| 3 |
+
{"id": "hotel", "prompt": "Você chegou em um hotel. Peça ajuda para encontrar a recepção.", "target_style": "português brasileiro, simples, uma ou duas frases", "answer_text": "Peça ajuda para encontrar a recepção.", "audio_frames": 38, "valid_mimi_frames": 38, "wav_path": "eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k/hotel.wav", "transcript": "Eu ajudo a para encontrar esse de passar", "whisper_error": "", "generated_audio": true, "non_empty_transcript": true, "looks_portuguese": false, "non_empty_answer_text": true, "pass_basic": true}
|
| 4 |
+
{"id": "aula", "prompt": "Explique para um aluno o que significa praticar uma língua todos os dias.", "target_style": "português brasileiro, didático, resposta curta", "answer_text": "Paraumum aluno o que significa praticar uma língua todos os dias is indeed a debated topic. In theory, it refers to the principal ingredient, which is a type of algae os, commonly found in the savanna, which is a herbaceous herb. There is no evidence to support this conclusion, as the principal ingredient, which is a", "audio_frames": 112, "valid_mimi_frames": 112, "wav_path": "eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k/aula.wav", "transcript": "Pera do mundo, o que se caparicar? Como alhar engloar todos os alhas enteidedadas? E inte...", "whisper_error": "", "generated_audio": true, "non_empty_transcript": true, "looks_portuguese": false, "non_empty_answer_text": true, "pass_basic": true}
|
| 5 |
+
{"id": "onibus", "prompt": "Você está no ponto de ônibus. Pergunte se aquele ônibus passa pelo centro.", "target_style": "português brasileiro, fala cotidiana", "answer_text": "Passaê está no pondto de ônibus.", "audio_frames": 38, "valid_mimi_frames": 38, "wav_path": "eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k/onibus.wav", "transcript": "Faça a pista, um ponto de Salibus.", "whisper_error": "", "generated_audio": true, "non_empty_transcript": true, "looks_portuguese": true, "non_empty_answer_text": true, "pass_basic": true}
|
eval/summary.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"rows": 5,
|
| 3 |
+
"audio_generated": 5,
|
| 4 |
+
"transcribed": 5,
|
| 5 |
+
"looks_portuguese": 3,
|
| 6 |
+
"basic_pass": 5,
|
| 7 |
+
"csv": "eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k/ptbr_talker_eval.csv",
|
| 8 |
+
"jsonl": "eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k/ptbr_talker_eval.jsonl"
|
| 9 |
+
}
|
file_manifest.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"path": "README.md",
|
| 4 |
+
"size": 2231
|
| 5 |
+
},
|
| 6 |
+
{
|
| 7 |
+
"path": "SHA256SUMS.txt",
|
| 8 |
+
"size": 467
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"path": "checkpoints/sft_ptbr_curriculum_100k_mixed_768.pth",
|
| 12 |
+
"size": 236159657
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"path": "checkpoints/sft_ptbr_curriculum_100k_t2a_repair_768.pth",
|
| 16 |
+
"size": 236160652
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"path": "checkpoints/sft_ptbr_curriculum_100k_textfix_768.pth",
|
| 20 |
+
"size": 236160055
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"path": "data/ptbr_curriculum_100k.jsonl",
|
| 24 |
+
"size": 63039962
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"path": "data/sft_text_ptbr_curriculum_100k.parquet",
|
| 28 |
+
"size": 6016108
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"path": "eval/aula.wav",
|
| 32 |
+
"size": 430124
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"path": "eval/hotel.wav",
|
| 36 |
+
"size": 145964
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"path": "eval/mercado.wav",
|
| 40 |
+
"size": 299564
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"path": "eval/onibus.wav",
|
| 44 |
+
"size": 145964
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"path": "eval/padaria.wav",
|
| 48 |
+
"size": 138284
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"path": "eval/ptbr_talker_eval.csv",
|
| 52 |
+
"size": 2165
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"path": "eval/ptbr_talker_eval.jsonl",
|
| 56 |
+
"size": 3242
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"path": "eval/summary.json",
|
| 60 |
+
"size": 277
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"path": "metadata.json",
|
| 64 |
+
"size": 372
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"path": "tools/build_ptbr_conversation_curriculum.py",
|
| 68 |
+
"size": 14188
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"path": "tools/eval_minimind_talker_ptbr.py",
|
| 72 |
+
"size": 7967
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"path": "tools/run_talker_finetune_phases.py",
|
| 76 |
+
"size": 15193
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"path": "tools/validate_talker_finetune_phases.py",
|
| 80 |
+
"size": 11771
|
| 81 |
+
}
|
| 82 |
+
]
|
metadata.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "minimind-o-ptbr-talker-curriculum-100k-poc",
|
| 3 |
+
"date": "2026-05-23",
|
| 4 |
+
"base": "MiniMind-O 768 native checkpoint",
|
| 5 |
+
"status": "experimental_poc",
|
| 6 |
+
"final_eval": {
|
| 7 |
+
"rows": 5,
|
| 8 |
+
"audio_generated": 5,
|
| 9 |
+
"transcribed": 5,
|
| 10 |
+
"looks_portuguese": 3,
|
| 11 |
+
"basic_pass": 5
|
| 12 |
+
},
|
| 13 |
+
"known_issue": "PT-BR quality and relevance still fail for production use."
|
| 14 |
+
}
|
tools/build_ptbr_conversation_curriculum.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import random
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import pyarrow as pa
|
| 7 |
+
import pyarrow.parquet as pq
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
SYSTEM_TUTOR = (
|
| 11 |
+
"Você é um tutor educacional em português brasileiro. "
|
| 12 |
+
"Responda sempre em português brasileiro, de forma curta, natural, correta e sem misturar idiomas."
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
SYSTEM_ROLEPLAY = (
|
| 16 |
+
"Você é um personagem em uma simulação de conversa cotidiana em português brasileiro. "
|
| 17 |
+
"Mantenha o papel, responda de forma curta e natural, e nunca misture inglês."
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
SCENARIOS = [
|
| 22 |
+
{
|
| 23 |
+
"name": "padaria",
|
| 24 |
+
"assistant_role": "atendente de padaria",
|
| 25 |
+
"user_role": "cliente",
|
| 26 |
+
"places": ["padaria do bairro", "balcão da padaria", "cafeteria pequena"],
|
| 27 |
+
"items": ["café com leite", "pão francês", "suco de laranja", "misto quente", "bolo de cenoura"],
|
| 28 |
+
"turns": [
|
| 29 |
+
("Bom dia, você pode me atender?", "Bom dia! Claro, em que posso ajudar?"),
|
| 30 |
+
("Quero {item}, por favor.", "Perfeito. Você prefere consumir aqui ou levar?"),
|
| 31 |
+
("Para levar. Quanto fica?", "Fica {price} reais. Posso colocar em uma sacola."),
|
| 32 |
+
("Pode sim. Aceita cartão?", "Aceitamos cartão, Pix e dinheiro."),
|
| 33 |
+
("Obrigado, tenha um bom dia.", "Eu que agradeço. Bom dia e volte sempre."),
|
| 34 |
+
],
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"name": "mercado",
|
| 38 |
+
"assistant_role": "funcionário de mercado",
|
| 39 |
+
"user_role": "cliente",
|
| 40 |
+
"places": ["mercado", "supermercado", "mercearia"],
|
| 41 |
+
"items": ["frutas", "arroz", "leite", "pão de forma", "produtos de limpeza"],
|
| 42 |
+
"turns": [
|
| 43 |
+
("Oi, você trabalha aqui?", "Trabalho sim. Como posso ajudar?"),
|
| 44 |
+
("Onde ficam as {item}?", "Ficam no corredor {aisle}, perto da entrada."),
|
| 45 |
+
("Tem alguma promoção hoje?", "Tem sim, algumas ofertas estão sinalizadas nas prateleiras."),
|
| 46 |
+
("Obrigado pela ajuda.", "Por nada. Qualquer dúvida, pode me chamar."),
|
| 47 |
+
("Vou procurar lá então.", "Certo. Espero que encontre o que precisa."),
|
| 48 |
+
],
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"name": "hotel",
|
| 52 |
+
"assistant_role": "recepcionista de hotel",
|
| 53 |
+
"user_role": "hóspede",
|
| 54 |
+
"places": ["hotel", "pousada", "recepção"],
|
| 55 |
+
"items": ["quarto", "café da manhã", "elevador", "reserva", "chave"],
|
| 56 |
+
"turns": [
|
| 57 |
+
("Boa noite, acabei de chegar.", "Boa noite! Seja bem-vindo. Você tem reserva?"),
|
| 58 |
+
("Tenho reserva em meu nome.", "Certo, posso conferir seu documento?"),
|
| 59 |
+
("Claro. Onde fica a recepção principal?", "Fica logo à direita, depois do sofá."),
|
| 60 |
+
("E o café da manhã?", "O café é servido das {hour} às {hour2}, no salão principal."),
|
| 61 |
+
("Obrigado pelas informações.", "Disponha. Tenha uma ótima estadia."),
|
| 62 |
+
],
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"name": "ônibus",
|
| 66 |
+
"assistant_role": "passageiro local",
|
| 67 |
+
"user_role": "pessoa no ponto",
|
| 68 |
+
"places": ["ponto de ônibus", "terminal", "rua principal"],
|
| 69 |
+
"items": ["centro", "shopping", "hospital", "faculdade", "estação"],
|
| 70 |
+
"turns": [
|
| 71 |
+
("Com licença, esse ônibus passa pelo {item}?", "Passa sim, mas ele dá uma volta antes."),
|
| 72 |
+
("Você sabe quanto tempo demora?", "Normalmente demora uns {minutes} minutos."),
|
| 73 |
+
("Preciso pagar em dinheiro?", "Pode pagar com cartão de transporte ou dinheiro."),
|
| 74 |
+
("Esse ponto é o correto?", "É sim. Você só precisa esperar a linha {line}."),
|
| 75 |
+
("Obrigado pela ajuda.", "De nada. Boa viagem."),
|
| 76 |
+
],
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"name": "farmácia",
|
| 80 |
+
"assistant_role": "atendente de farmácia",
|
| 81 |
+
"user_role": "cliente",
|
| 82 |
+
"places": ["farmácia", "drogaria", "balcão"],
|
| 83 |
+
"items": ["protetor solar", "curativo", "soro fisiológico", "termômetro", "vitamina"],
|
| 84 |
+
"turns": [
|
| 85 |
+
("Boa tarde, preciso de ajuda.", "Boa tarde. O que você procura?"),
|
| 86 |
+
("Estou procurando {item}.", "Temos sim. Fica naquela prateleira à esquerda."),
|
| 87 |
+
("Você pode me mostrar?", "Claro, eu acompanho você até lá."),
|
| 88 |
+
("Tem uma opção mais barata?", "Temos esta opção, que costuma sair por {price} reais."),
|
| 89 |
+
("Obrigado, vou levar.", "Perfeito. Pode passar no caixa quando quiser."),
|
| 90 |
+
],
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"name": "restaurante",
|
| 94 |
+
"assistant_role": "garçom",
|
| 95 |
+
"user_role": "cliente",
|
| 96 |
+
"places": ["restaurante", "lanchonete", "mesa"],
|
| 97 |
+
"items": ["prato do dia", "água sem gás", "suco natural", "salada", "sobremesa"],
|
| 98 |
+
"turns": [
|
| 99 |
+
("Olá, posso fazer um pedido?", "Claro. O que você gostaria?"),
|
| 100 |
+
("Quero {item}, por favor.", "Ótima escolha. Deseja algo para beber?"),
|
| 101 |
+
("Uma água, por favor.", "Certo. Vou trazer seu pedido em alguns minutos."),
|
| 102 |
+
("Quanto tempo demora?", "Deve levar cerca de {minutes} minutos."),
|
| 103 |
+
("Tudo bem, obrigado.", "Eu que agradeço. Já volto com o pedido."),
|
| 104 |
+
],
|
| 105 |
+
},
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
TUTOR_FACTS = [
|
| 110 |
+
("fotossíntese", "Fotossíntese é quando a planta usa luz para produzir alimento.", "ciências"),
|
| 111 |
+
("evaporação", "Evaporação é quando a água líquida vira vapor.", "ciências"),
|
| 112 |
+
("substantivo", "Substantivo é a palavra que nomeia seres, lugares, objetos ou ideias.", "português"),
|
| 113 |
+
("verbo", "Verbo indica ação, estado ou fenômeno da natureza.", "português"),
|
| 114 |
+
("democracia", "Democracia é um regime em que o povo participa das decisões.", "história"),
|
| 115 |
+
("paisagem", "Paisagem é tudo que podemos observar em um lugar.", "geografia"),
|
| 116 |
+
("resumo", "Resumo reúne as ideias principais de um texto.", "hábitos de estudo"),
|
| 117 |
+
("revisão", "Revisão ajuda a lembrar e organizar o conteúdo estudado.", "hábitos de estudo"),
|
| 118 |
+
]
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def render(template, rng):
|
| 122 |
+
return template.format(
|
| 123 |
+
item=rng.choice(
|
| 124 |
+
[
|
| 125 |
+
"frutas", "café", "pão", "água", "reserva", "centro", "curativo", "leite",
|
| 126 |
+
"queijo", "bolo", "arroz", "feijão", "maçãs", "banana", "remédio",
|
| 127 |
+
"protetor solar", "sabonete", "lanche", "suco", "salada", "sobremesa",
|
| 128 |
+
"elevador", "recepção", "terminal", "hospital", "faculdade", "shopping",
|
| 129 |
+
]
|
| 130 |
+
),
|
| 131 |
+
price=f"{rng.randint(6, 89)}",
|
| 132 |
+
aisle=rng.randint(1, 48),
|
| 133 |
+
hour=rng.choice(["seis", "sete", "oito", "nove", "dez"]),
|
| 134 |
+
hour2=rng.choice(["dez", "onze", "meio-dia"]),
|
| 135 |
+
minutes=str(rng.randint(4, 55)),
|
| 136 |
+
line=str(rng.randint(100, 899)),
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def normalize(text):
|
| 141 |
+
return " ".join(text.strip().split())
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def existing_qa_rows(paths):
|
| 145 |
+
for path in paths:
|
| 146 |
+
p = Path(path)
|
| 147 |
+
if not p.exists():
|
| 148 |
+
continue
|
| 149 |
+
with p.open("r", encoding="utf-8") as f:
|
| 150 |
+
for line in f:
|
| 151 |
+
if not line.strip():
|
| 152 |
+
continue
|
| 153 |
+
try:
|
| 154 |
+
row = json.loads(line)
|
| 155 |
+
except Exception:
|
| 156 |
+
continue
|
| 157 |
+
q = normalize(row.get("question") or row.get("prompt") or "")
|
| 158 |
+
a = normalize(row.get("answer") or row.get("response") or "")
|
| 159 |
+
if q and a and len(a.split()) <= 30:
|
| 160 |
+
yield {
|
| 161 |
+
"source": "existing_qa",
|
| 162 |
+
"subject": row.get("subject", "geral"),
|
| 163 |
+
"conversations": [
|
| 164 |
+
{"role": "system", "content": SYSTEM_TUTOR},
|
| 165 |
+
{"role": "user", "content": q},
|
| 166 |
+
{"role": "assistant", "content": a},
|
| 167 |
+
],
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def tutor_row(rng):
|
| 172 |
+
term, answer, subject = rng.choice(TUTOR_FACTS)
|
| 173 |
+
question = rng.choice(
|
| 174 |
+
[
|
| 175 |
+
f"O que é {term}?",
|
| 176 |
+
f"Explique {term} de forma simples.",
|
| 177 |
+
f"Me diga em poucas palavras o que significa {term}.",
|
| 178 |
+
f"Como eu posso entender {term}?",
|
| 179 |
+
]
|
| 180 |
+
)
|
| 181 |
+
return {
|
| 182 |
+
"source": "synthetic_tutor",
|
| 183 |
+
"subject": subject,
|
| 184 |
+
"conversations": [
|
| 185 |
+
{"role": "system", "content": SYSTEM_TUTOR},
|
| 186 |
+
{"role": "user", "content": question},
|
| 187 |
+
{"role": "assistant", "content": answer},
|
| 188 |
+
],
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def roleplay_row(rng):
|
| 193 |
+
scenario = rng.choice(SCENARIOS)
|
| 194 |
+
detail = rng.choice(
|
| 195 |
+
[
|
| 196 |
+
"em uma manhã movimentada",
|
| 197 |
+
"no fim da tarde",
|
| 198 |
+
"com pouco tempo disponível",
|
| 199 |
+
"falando com calma",
|
| 200 |
+
"em uma situação de prática de idioma",
|
| 201 |
+
"com atendimento educado",
|
| 202 |
+
]
|
| 203 |
+
)
|
| 204 |
+
neighborhood = rng.choice(
|
| 205 |
+
[
|
| 206 |
+
"Centro", "Liberdade", "Pinheiros", "Savassi", "Boa Viagem", "Botafogo",
|
| 207 |
+
"Moema", "Copacabana", "Água Verde", "Asa Norte", "Ponta Verde",
|
| 208 |
+
]
|
| 209 |
+
)
|
| 210 |
+
system = (
|
| 211 |
+
f"{SYSTEM_ROLEPLAY} Cenário: {rng.choice(scenario['places'])}. "
|
| 212 |
+
f"Seu papel: {scenario['assistant_role']}. Papel do usuário: {scenario['user_role']}. "
|
| 213 |
+
f"Detalhe: {detail}, no bairro {neighborhood}."
|
| 214 |
+
)
|
| 215 |
+
turns = [{"role": "system", "content": system}]
|
| 216 |
+
for user, assistant in scenario["turns"]:
|
| 217 |
+
turns.append({"role": "user", "content": render(user, rng)})
|
| 218 |
+
turns.append({"role": "assistant", "content": render(assistant, rng)})
|
| 219 |
+
return {
|
| 220 |
+
"source": "synthetic_roleplay",
|
| 221 |
+
"subject": scenario["name"],
|
| 222 |
+
"conversations": turns,
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def anti_english_row(rng):
|
| 227 |
+
topic = rng.choice(["padaria", "hotel", "mercado", "ônibus", "aula de português"])
|
| 228 |
+
user = rng.choice(
|
| 229 |
+
[
|
| 230 |
+
f"Can you answer in English about {topic}?",
|
| 231 |
+
f"Explique em português, mas eu escrevi market e bus sem querer.",
|
| 232 |
+
f"Responda só em português: how do I ask for help in a {topic}?",
|
| 233 |
+
f"Estou praticando português. Não use inglês quando falar de {topic}.",
|
| 234 |
+
]
|
| 235 |
+
)
|
| 236 |
+
assistant = rng.choice(
|
| 237 |
+
[
|
| 238 |
+
"Claro. Vamos praticar só em português brasileiro, com frases curtas e naturais.",
|
| 239 |
+
"Tudo bem. Vou responder apenas em português brasileiro e manter a conversa simples.",
|
| 240 |
+
"Combinado. Vou evitar inglês e seguir o contexto da situação.",
|
| 241 |
+
"Perfeito. Vamos continuar em português brasileiro, de forma clara e natural.",
|
| 242 |
+
]
|
| 243 |
+
)
|
| 244 |
+
return {
|
| 245 |
+
"source": "anti_english",
|
| 246 |
+
"subject": topic,
|
| 247 |
+
"conversations": [
|
| 248 |
+
{"role": "system", "content": SYSTEM_ROLEPLAY},
|
| 249 |
+
{"role": "user", "content": user},
|
| 250 |
+
{"role": "assistant", "content": assistant},
|
| 251 |
+
],
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def math_row(rng):
|
| 256 |
+
a = rng.randint(2, 999)
|
| 257 |
+
b = rng.randint(2, 999)
|
| 258 |
+
op = rng.choice(["mais", "menos", "vezes"])
|
| 259 |
+
if op == "mais":
|
| 260 |
+
answer = f"{a} mais {b} é {a + b}."
|
| 261 |
+
elif op == "menos":
|
| 262 |
+
x, y = max(a, b), min(a, b)
|
| 263 |
+
answer = f"{x} menos {y} é {x - y}."
|
| 264 |
+
a, b = x, y
|
| 265 |
+
else:
|
| 266 |
+
a = rng.randint(2, 50)
|
| 267 |
+
b = rng.randint(2, 50)
|
| 268 |
+
answer = f"{a} vezes {b} é {a * b}."
|
| 269 |
+
return {
|
| 270 |
+
"source": "synthetic_math",
|
| 271 |
+
"subject": "matemática",
|
| 272 |
+
"conversations": [
|
| 273 |
+
{"role": "system", "content": SYSTEM_TUTOR},
|
| 274 |
+
{"role": "user", "content": f"Quanto é {a} {op} {b}?"},
|
| 275 |
+
{"role": "assistant", "content": answer},
|
| 276 |
+
],
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def row_key(row):
|
| 281 |
+
return json.dumps(row["conversations"], ensure_ascii=False, sort_keys=True)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def build_rows(args):
|
| 285 |
+
rng = random.Random(args.seed)
|
| 286 |
+
rows = []
|
| 287 |
+
seen = set()
|
| 288 |
+
|
| 289 |
+
for row in existing_qa_rows(args.include_jsonl):
|
| 290 |
+
key = row_key(row)
|
| 291 |
+
if key not in seen:
|
| 292 |
+
seen.add(key)
|
| 293 |
+
rows.append(row)
|
| 294 |
+
if len(rows) >= args.count:
|
| 295 |
+
return rows
|
| 296 |
+
|
| 297 |
+
generators = [roleplay_row, roleplay_row, anti_english_row, tutor_row, math_row]
|
| 298 |
+
attempts = 0
|
| 299 |
+
while len(rows) < args.count and attempts < args.count * 40:
|
| 300 |
+
attempts += 1
|
| 301 |
+
row = rng.choice(generators)(rng)
|
| 302 |
+
key = row_key(row)
|
| 303 |
+
if key in seen:
|
| 304 |
+
continue
|
| 305 |
+
seen.add(key)
|
| 306 |
+
rows.append(row)
|
| 307 |
+
return rows
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def write_jsonl(rows, path):
|
| 311 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 312 |
+
with path.open("w", encoding="utf-8") as f:
|
| 313 |
+
for row in rows:
|
| 314 |
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def write_parquet(rows, path):
|
| 318 |
+
conversations = [json.dumps(row["conversations"], ensure_ascii=False) for row in rows]
|
| 319 |
+
metadata = [json.dumps({"source": row["source"], "subject": row["subject"]}, ensure_ascii=False) for row in rows]
|
| 320 |
+
n = len(rows)
|
| 321 |
+
table = pa.table(
|
| 322 |
+
{
|
| 323 |
+
"conversations": pa.array(conversations, type=pa.large_string()),
|
| 324 |
+
"answer_audios": pa.array([[] for _ in range(n)], type=pa.list_(pa.list_(pa.int64()))),
|
| 325 |
+
"question_audios": pa.array([[] for _ in range(n)]),
|
| 326 |
+
"ref_audios": pa.array([[] for _ in range(n)], type=pa.list_(pa.int64())),
|
| 327 |
+
"spk_emb": pa.array([[] for _ in range(n)], type=pa.list_(pa.float64())),
|
| 328 |
+
"metadata": pa.array(metadata, type=pa.large_string()),
|
| 329 |
+
}
|
| 330 |
+
)
|
| 331 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 332 |
+
pq.write_table(table, path)
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def parse_args():
|
| 336 |
+
parser = argparse.ArgumentParser(description="Build a larger PT-BR text SFT curriculum.")
|
| 337 |
+
parser.add_argument("--count", type=int, default=100000)
|
| 338 |
+
parser.add_argument("--seed", type=int, default=20260523)
|
| 339 |
+
parser.add_argument("--include-jsonl", nargs="*", default=["data_ptbr/manifests/tutor_text_sft_50k.jsonl"])
|
| 340 |
+
parser.add_argument("--out-jsonl", default="data_ptbr/manifests/ptbr_curriculum_100k.jsonl")
|
| 341 |
+
parser.add_argument("--out-parquet", default="data_ptbr/parquet/sft_text_ptbr_curriculum_100k.parquet")
|
| 342 |
+
return parser.parse_args()
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def main():
|
| 346 |
+
args = parse_args()
|
| 347 |
+
rows = build_rows(args)
|
| 348 |
+
write_jsonl(rows, Path(args.out_jsonl))
|
| 349 |
+
write_parquet(rows, Path(args.out_parquet))
|
| 350 |
+
print(json.dumps({"rows": len(rows), "jsonl": args.out_jsonl, "parquet": args.out_parquet}, ensure_ascii=False))
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
if __name__ == "__main__":
|
| 354 |
+
main()
|
tools/eval_minimind_talker_ptbr.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import csv
|
| 3 |
+
import json
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import soundfile as sf
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
if str(REPO_ROOT) not in sys.path:
|
| 13 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 14 |
+
|
| 15 |
+
from eval_omni import init_model # noqa: E402
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
PROMPTS = [
|
| 19 |
+
{
|
| 20 |
+
"id": "padaria",
|
| 21 |
+
"prompt": "Você está em uma padaria no Brasil. Cumprimente o atendente e peça um café com leite.",
|
| 22 |
+
"target_style": "português brasileiro, curto, conversa cotidiana",
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"id": "mercado",
|
| 26 |
+
"prompt": "Você está no mercado. Pergunte onde ficam as frutas e agradeça de forma natural.",
|
| 27 |
+
"target_style": "português brasileiro, educado, sem inglês",
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"id": "hotel",
|
| 31 |
+
"prompt": "Você chegou em um hotel. Peça ajuda para encontrar a recepção.",
|
| 32 |
+
"target_style": "português brasileiro, simples, uma ou duas frases",
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "aula",
|
| 36 |
+
"prompt": "Explique para um aluno o que significa praticar uma língua todos os dias.",
|
| 37 |
+
"target_style": "português brasileiro, didático, resposta curta",
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"id": "onibus",
|
| 41 |
+
"prompt": "Você está no ponto de ônibus. Pergunte se aquele ônibus passa pelo centro.",
|
| 42 |
+
"target_style": "português brasileiro, fala cotidiana",
|
| 43 |
+
},
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def clean_answer(tokenizer, token_ids):
|
| 48 |
+
text = tokenizer.decode(token_ids, skip_special_tokens=True)
|
| 49 |
+
return " ".join(text.replace("\n", " ").split())
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def generate_one(model, tokenizer, args, sample):
|
| 53 |
+
messages = [{"role": "user", "content": sample["prompt"]}]
|
| 54 |
+
inputs_text = tokenizer.apply_chat_template(
|
| 55 |
+
messages,
|
| 56 |
+
tokenize=False,
|
| 57 |
+
add_generation_prompt=True,
|
| 58 |
+
open_thinking=False,
|
| 59 |
+
)
|
| 60 |
+
input_ids = tokenizer(inputs_text).data["input_ids"]
|
| 61 |
+
x = torch.tensor(input_ids, dtype=torch.long, device=args.device)[None, ...]
|
| 62 |
+
|
| 63 |
+
audio_frames = []
|
| 64 |
+
last_tokens = None
|
| 65 |
+
with torch.no_grad():
|
| 66 |
+
iterator = model.generate(
|
| 67 |
+
x,
|
| 68 |
+
tokenizer.eos_token_id,
|
| 69 |
+
max_new_tokens=args.max_new_tokens,
|
| 70 |
+
temperature=args.temperature,
|
| 71 |
+
top_p=args.top_p,
|
| 72 |
+
stream=True,
|
| 73 |
+
return_audio_codes=True,
|
| 74 |
+
open_thinking=False,
|
| 75 |
+
audio_inputs=None,
|
| 76 |
+
audio_lens=None,
|
| 77 |
+
pixel_values=None,
|
| 78 |
+
ref_codes=None,
|
| 79 |
+
spk_emb=None,
|
| 80 |
+
)
|
| 81 |
+
for token_ids, audio_frame in iterator:
|
| 82 |
+
if token_ids is not None:
|
| 83 |
+
last_tokens = token_ids[0].tolist()
|
| 84 |
+
if audio_frame:
|
| 85 |
+
audio_frames.append(audio_frame)
|
| 86 |
+
|
| 87 |
+
answer_text = clean_answer(tokenizer, last_tokens or [])
|
| 88 |
+
codes = [frame for frame in audio_frames if frame and len(frame) == 8]
|
| 89 |
+
wav_path = None
|
| 90 |
+
|
| 91 |
+
if codes:
|
| 92 |
+
mimi_codes = torch.tensor(codes, dtype=torch.long).T.unsqueeze(0).to(args.device)
|
| 93 |
+
filtered = torch.where(mimi_codes >= 2049, torch.zeros_like(mimi_codes), mimi_codes)
|
| 94 |
+
with torch.no_grad():
|
| 95 |
+
audio = model.mimi_model.decode(filtered).audio_values
|
| 96 |
+
wav_path = Path(args.output_dir) / f"{sample['id']}.wav"
|
| 97 |
+
sf.write(wav_path, audio.squeeze().detach().float().cpu().numpy(), args.sample_rate)
|
| 98 |
+
|
| 99 |
+
return {
|
| 100 |
+
"id": sample["id"],
|
| 101 |
+
"prompt": sample["prompt"],
|
| 102 |
+
"target_style": sample["target_style"],
|
| 103 |
+
"answer_text": answer_text,
|
| 104 |
+
"audio_frames": len(audio_frames),
|
| 105 |
+
"valid_mimi_frames": len(codes),
|
| 106 |
+
"wav_path": str(wav_path) if wav_path else "",
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def transcribe_rows(rows, args):
|
| 111 |
+
try:
|
| 112 |
+
import whisper
|
| 113 |
+
except Exception as exc:
|
| 114 |
+
for row in rows:
|
| 115 |
+
row["whisper_error"] = f"whisper unavailable: {exc}"
|
| 116 |
+
return rows
|
| 117 |
+
|
| 118 |
+
model = whisper.load_model(args.whisper_model)
|
| 119 |
+
for row in rows:
|
| 120 |
+
wav_path = row.get("wav_path")
|
| 121 |
+
if not wav_path:
|
| 122 |
+
row["transcript"] = ""
|
| 123 |
+
row["whisper_error"] = "no wav generated"
|
| 124 |
+
continue
|
| 125 |
+
try:
|
| 126 |
+
result = model.transcribe(wav_path, language="pt", fp16=args.device.startswith("cuda"))
|
| 127 |
+
row["transcript"] = " ".join(result.get("text", "").split())
|
| 128 |
+
row["whisper_error"] = ""
|
| 129 |
+
except Exception as exc:
|
| 130 |
+
row["transcript"] = ""
|
| 131 |
+
row["whisper_error"] = str(exc)
|
| 132 |
+
return rows
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def classify_row(row):
|
| 136 |
+
transcript = (row.get("transcript") or "").strip()
|
| 137 |
+
answer = (row.get("answer_text") or "").strip()
|
| 138 |
+
generated_audio = bool(row.get("wav_path")) and Path(row["wav_path"]).exists()
|
| 139 |
+
has_pt_hint = any(ch in transcript.lower() for ch in "ãõçáéíóúâêôà") or any(
|
| 140 |
+
word in transcript.lower().split()
|
| 141 |
+
for word in ["voce", "você", "obrigado", "onde", "cafe", "café", "onibus", "ônibus", "centro"]
|
| 142 |
+
)
|
| 143 |
+
row["generated_audio"] = generated_audio
|
| 144 |
+
row["non_empty_transcript"] = bool(transcript)
|
| 145 |
+
row["looks_portuguese"] = bool(transcript and has_pt_hint)
|
| 146 |
+
row["non_empty_answer_text"] = bool(answer)
|
| 147 |
+
row["pass_basic"] = bool(generated_audio and transcript and answer)
|
| 148 |
+
return row
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def write_outputs(rows, args):
|
| 152 |
+
out_dir = Path(args.output_dir)
|
| 153 |
+
jsonl_path = out_dir / "ptbr_talker_eval.jsonl"
|
| 154 |
+
csv_path = out_dir / "ptbr_talker_eval.csv"
|
| 155 |
+
summary_path = out_dir / "summary.json"
|
| 156 |
+
|
| 157 |
+
with jsonl_path.open("w", encoding="utf-8") as f:
|
| 158 |
+
for row in rows:
|
| 159 |
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 160 |
+
|
| 161 |
+
fieldnames = sorted({key for row in rows for key in row})
|
| 162 |
+
with csv_path.open("w", encoding="utf-8", newline="") as f:
|
| 163 |
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
| 164 |
+
writer.writeheader()
|
| 165 |
+
writer.writerows(rows)
|
| 166 |
+
|
| 167 |
+
summary = {
|
| 168 |
+
"rows": len(rows),
|
| 169 |
+
"audio_generated": sum(1 for row in rows if row.get("generated_audio")),
|
| 170 |
+
"transcribed": sum(1 for row in rows if row.get("non_empty_transcript")),
|
| 171 |
+
"looks_portuguese": sum(1 for row in rows if row.get("looks_portuguese")),
|
| 172 |
+
"basic_pass": sum(1 for row in rows if row.get("pass_basic")),
|
| 173 |
+
"csv": str(csv_path),
|
| 174 |
+
"jsonl": str(jsonl_path),
|
| 175 |
+
}
|
| 176 |
+
summary_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 177 |
+
return summary
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def parse_args():
|
| 181 |
+
parser = argparse.ArgumentParser(description="Generate and transcribe PT-BR MiniMind Talker samples.")
|
| 182 |
+
parser.add_argument("--load_from", default="model")
|
| 183 |
+
parser.add_argument("--save_dir", default="out")
|
| 184 |
+
parser.add_argument("--weight", default="sft_omni_ptbr_talker")
|
| 185 |
+
parser.add_argument("--hidden_size", type=int, default=768)
|
| 186 |
+
parser.add_argument("--num_hidden_layers", type=int, default=8)
|
| 187 |
+
parser.add_argument("--use_moe", type=int, default=0)
|
| 188 |
+
parser.add_argument("--output-dir", default="eval_ptbr/minimind_talker_ptbr_eval")
|
| 189 |
+
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 190 |
+
parser.add_argument("--max-new-tokens", type=int, default=180)
|
| 191 |
+
parser.add_argument("--temperature", type=float, default=0.55)
|
| 192 |
+
parser.add_argument("--top-p", type=float, default=0.8)
|
| 193 |
+
parser.add_argument("--sample-rate", type=int, default=24000)
|
| 194 |
+
parser.add_argument("--whisper-model", default="medium")
|
| 195 |
+
parser.add_argument("--limit", type=int, default=len(PROMPTS))
|
| 196 |
+
return parser.parse_args()
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def main():
|
| 200 |
+
args = parse_args()
|
| 201 |
+
Path(args.output_dir).mkdir(parents=True, exist_ok=True)
|
| 202 |
+
model, tokenizer = init_model(args)
|
| 203 |
+
|
| 204 |
+
rows = []
|
| 205 |
+
for sample in PROMPTS[: args.limit]:
|
| 206 |
+
print(f"[eval] {sample['id']}: {sample['prompt']}", flush=True)
|
| 207 |
+
row = generate_one(model, tokenizer, args, sample)
|
| 208 |
+
rows.append(row)
|
| 209 |
+
|
| 210 |
+
rows = transcribe_rows(rows, args)
|
| 211 |
+
rows = [classify_row(row) for row in rows]
|
| 212 |
+
summary = write_outputs(rows, args)
|
| 213 |
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
if __name__ == "__main__":
|
| 217 |
+
main()
|
tools/run_talker_finetune_phases.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import shlex
|
| 4 |
+
import subprocess
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
PHASES = [
|
| 9 |
+
{
|
| 10 |
+
"id": "phase0_preflight",
|
| 11 |
+
"title": "Preflight remoto",
|
| 12 |
+
"estimate": "2 min",
|
| 13 |
+
"gpu_hours": 0.0,
|
| 14 |
+
"kind": "validation",
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"id": "phase1_poc_tucano_q4",
|
| 18 |
+
"title": "POC texto Tucano2 Q4",
|
| 19 |
+
"estimate": "3-5 min",
|
| 20 |
+
"gpu_hours": 0.05,
|
| 21 |
+
"kind": "benchmark",
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"id": "phase2_text_mimi_manifest",
|
| 25 |
+
"title": "Dataset texto -> Mimi",
|
| 26 |
+
"estimate": "1-3 min",
|
| 27 |
+
"gpu_hours": 0.0,
|
| 28 |
+
"kind": "data",
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"id": "phase3_minimind_talker_t2a",
|
| 32 |
+
"title": "Fine-tune MiniMind Talker T2A",
|
| 33 |
+
"estimate": "1-3 h",
|
| 34 |
+
"gpu_hours": 2.0,
|
| 35 |
+
"kind": "train",
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"id": "phase4_minimind_eval_whisper",
|
| 39 |
+
"title": "Avaliação Mimi + Whisper",
|
| 40 |
+
"estimate": "15-40 min",
|
| 41 |
+
"gpu_hours": 0.5,
|
| 42 |
+
"kind": "eval",
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"id": "phase5_simple_text_mimi_talker",
|
| 46 |
+
"title": "Protótipo Talker separado",
|
| 47 |
+
"estimate": "20-90 min",
|
| 48 |
+
"gpu_hours": 1.0,
|
| 49 |
+
"kind": "train",
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"id": "phase6_report",
|
| 53 |
+
"title": "Relatório e página",
|
| 54 |
+
"estimate": "2 min",
|
| 55 |
+
"gpu_hours": 0.0,
|
| 56 |
+
"kind": "report",
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"id": "phase7_ptbr_curriculum_dataset",
|
| 60 |
+
"title": "Currículo PT-BR 100k",
|
| 61 |
+
"estimate": "2-5 min",
|
| 62 |
+
"gpu_hours": 0.0,
|
| 63 |
+
"kind": "data",
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"id": "phase8_ptbr_textfix_sft",
|
| 67 |
+
"title": "SFT textual PT-BR ampliado",
|
| 68 |
+
"estimate": "30-90 min",
|
| 69 |
+
"gpu_hours": 0.8,
|
| 70 |
+
"kind": "train",
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"id": "phase9_ptbr_t2a_repair",
|
| 74 |
+
"title": "Reparo T2A após textfix",
|
| 75 |
+
"estimate": "5-20 min",
|
| 76 |
+
"gpu_hours": 0.2,
|
| 77 |
+
"kind": "train",
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"id": "phase10_ptbr_mixed_sft",
|
| 81 |
+
"title": "SFT misto texto + áudio",
|
| 82 |
+
"estimate": "40-120 min",
|
| 83 |
+
"gpu_hours": 1.0,
|
| 84 |
+
"kind": "train",
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"id": "phase11_ptbr_eval_after_mixed",
|
| 88 |
+
"title": "Reavaliação PT-BR pós-misto",
|
| 89 |
+
"estimate": "10-30 min",
|
| 90 |
+
"gpu_hours": 0.4,
|
| 91 |
+
"kind": "eval",
|
| 92 |
+
},
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def shell_join(parts):
|
| 97 |
+
return " ".join(shlex.quote(str(part)) for part in parts)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def remote_command(args, script):
|
| 101 |
+
return ["ssh", "-p", str(args.ssh_port), args.remote, f"bash -lc {shlex.quote(script)}"]
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def phase_script(args, phase_id):
|
| 105 |
+
repo = shlex.quote(args.repo)
|
| 106 |
+
if phase_id == "phase0_preflight":
|
| 107 |
+
return (
|
| 108 |
+
f"cd {repo} && "
|
| 109 |
+
"nvidia-smi --query-gpu=name,memory.used,memory.total,utilization.gpu --format=csv,noheader && "
|
| 110 |
+
"test -f trainer/train_sft_omni.py && "
|
| 111 |
+
f"test -f {shlex.quote(args.t2a_parquet)} && "
|
| 112 |
+
f"test -f out/{shlex.quote(args.from_weight)}_768.pth && "
|
| 113 |
+
"test -f tools_ptbr/eval_generated_with_whisper.py && "
|
| 114 |
+
"echo PHASE0_OK"
|
| 115 |
+
)
|
| 116 |
+
if phase_id == "phase1_poc_tucano_q4":
|
| 117 |
+
return (
|
| 118 |
+
f"cd {repo} && "
|
| 119 |
+
"CUDA_VISIBLE_DEVICES=0 .venv/bin/python tools_ptbr/poc_voice/run_tucano2_tts_poc.py "
|
| 120 |
+
f"--merged-model {shlex.quote(args.tucano_merged_model)} "
|
| 121 |
+
"--quantization bnb4 --bnb-4bit-use-double-quant "
|
| 122 |
+
"--scenarios padaria hotel mercado "
|
| 123 |
+
"--tts-backend none "
|
| 124 |
+
f"--out-dir {shlex.quote(args.poc_out_dir)}"
|
| 125 |
+
)
|
| 126 |
+
if phase_id == "phase2_text_mimi_manifest":
|
| 127 |
+
return (
|
| 128 |
+
f"cd {repo} && "
|
| 129 |
+
".venv/bin/python tools_ptbr/talker_text_mimi/build_text_mimi_manifest.py "
|
| 130 |
+
f"--input-parquet {shlex.quote(args.t2a_parquet)} "
|
| 131 |
+
f"--out-jsonl {shlex.quote(args.text_mimi_manifest)} "
|
| 132 |
+
"--min-frames 8 --max-frames 120"
|
| 133 |
+
)
|
| 134 |
+
if phase_id == "phase3_minimind_talker_t2a":
|
| 135 |
+
return (
|
| 136 |
+
f"cd {repo}/trainer && "
|
| 137 |
+
"CUDA_VISIBLE_DEVICES=0 ../.venv/bin/python -m torch.distributed.run "
|
| 138 |
+
f"--master_port {args.master_port} --nproc_per_node 1 train_sft_omni.py "
|
| 139 |
+
f"--learning_rate {args.talker_lr} "
|
| 140 |
+
f"--data_path {shlex.quote('../' + args.t2a_parquet if not args.t2a_parquet.startswith('../') else args.t2a_parquet)} "
|
| 141 |
+
f"--epochs {args.talker_epochs} "
|
| 142 |
+
f"--batch_size {args.talker_batch_size} "
|
| 143 |
+
f"--accumulation_steps {args.talker_accumulation_steps} "
|
| 144 |
+
f"--max_seq_len {args.max_seq_len} "
|
| 145 |
+
"--use_compile 0 "
|
| 146 |
+
f"--from_weight {shlex.quote(args.from_weight)} "
|
| 147 |
+
f"--save_weight {shlex.quote(args.save_weight)} "
|
| 148 |
+
"--freeze_backbone all --mode all --use_moe 0 "
|
| 149 |
+
"--grad_clip 1.0 --save_interval 500 --log_interval 50"
|
| 150 |
+
)
|
| 151 |
+
if phase_id == "phase4_minimind_eval_whisper":
|
| 152 |
+
return (
|
| 153 |
+
f"cd {repo} && "
|
| 154 |
+
".venv/bin/python tools_ptbr/eval_minimind_talker_ptbr.py --load_from model --save_dir out "
|
| 155 |
+
f"--weight {shlex.quote(args.save_weight)} "
|
| 156 |
+
"--hidden_size 768 --num_hidden_layers 8 --use_moe 0 "
|
| 157 |
+
"--temperature 0.55 --top-p 0.8 --max-new-tokens 180 "
|
| 158 |
+
"--whisper-model medium "
|
| 159 |
+
f"--output-dir {shlex.quote(args.minimind_eval_dir)}"
|
| 160 |
+
)
|
| 161 |
+
if phase_id == "phase5_simple_text_mimi_talker":
|
| 162 |
+
return (
|
| 163 |
+
f"cd {repo} && "
|
| 164 |
+
".venv/bin/python tools_ptbr/talker_text_mimi/train_simple_text_mimi_talker.py "
|
| 165 |
+
f"--train-jsonl {shlex.quote(args.text_mimi_manifest)} "
|
| 166 |
+
f"--out-dir {shlex.quote(args.simple_talker_out_dir)} "
|
| 167 |
+
"--tokenizer Polygl0t/Tucano2-qwen-1.5B-Instruct "
|
| 168 |
+
"--max-text-tokens 96 --max-frames 96 "
|
| 169 |
+
"--text-dim 384 --hidden-size 512 --layers 4 --heads 8 "
|
| 170 |
+
f"--epochs {args.simple_talker_epochs} "
|
| 171 |
+
f"--batch-size {args.simple_talker_batch_size} "
|
| 172 |
+
"--grad-accum 4 --learning-rate 3e-4 --log-steps 20 --fp16"
|
| 173 |
+
)
|
| 174 |
+
if phase_id == "phase6_report":
|
| 175 |
+
return (
|
| 176 |
+
f"cd {repo} && "
|
| 177 |
+
"python - <<'PY'\n"
|
| 178 |
+
"import json\n"
|
| 179 |
+
"from pathlib import Path\n"
|
| 180 |
+
"items = {}\n"
|
| 181 |
+
"for path in ['eval_ptbr/poc_voice_tucano_sft_q4/smoke_missing', "
|
| 182 |
+
f"{args.poc_out_dir!r} + '/summary.json', "
|
| 183 |
+
f"{args.minimind_eval_dir!r} + '/summary.json', "
|
| 184 |
+
f"{args.minimind_eval_dir!r} + '/ptbr_talker_eval.csv', "
|
| 185 |
+
f"{args.simple_talker_out_dir!r} + '/talker.pt']:\n"
|
| 186 |
+
" p = Path(path)\n"
|
| 187 |
+
" items[str(p)] = p.exists()\n"
|
| 188 |
+
"Path('eval_ptbr/talker_finetune_phase_status.json').write_text(json.dumps(items, indent=2), encoding='utf-8')\n"
|
| 189 |
+
"print(json.dumps(items, indent=2))\n"
|
| 190 |
+
"PY"
|
| 191 |
+
)
|
| 192 |
+
if phase_id == "phase7_ptbr_curriculum_dataset":
|
| 193 |
+
return (
|
| 194 |
+
f"cd {repo} && "
|
| 195 |
+
".venv/bin/python tools_ptbr/build_ptbr_conversation_curriculum.py "
|
| 196 |
+
f"--count {args.curriculum_count} "
|
| 197 |
+
f"--out-jsonl {shlex.quote(args.curriculum_jsonl)} "
|
| 198 |
+
f"--out-parquet {shlex.quote(args.curriculum_parquet)} "
|
| 199 |
+
"--include-jsonl data_ptbr/manifests/tutor_text_sft_50k.jsonl"
|
| 200 |
+
)
|
| 201 |
+
if phase_id == "phase8_ptbr_textfix_sft":
|
| 202 |
+
return (
|
| 203 |
+
f"cd {repo}/trainer && "
|
| 204 |
+
"CUDA_VISIBLE_DEVICES=0 ../.venv/bin/python -m torch.distributed.run "
|
| 205 |
+
f"--master_port {args.textfix_master_port} --nproc_per_node 1 train_sft_omni.py "
|
| 206 |
+
"--learning_rate 1e-5 "
|
| 207 |
+
f"--data_path {shlex.quote('../' + args.curriculum_parquet if not args.curriculum_parquet.startswith('../') else args.curriculum_parquet)} "
|
| 208 |
+
"--epochs 1 --batch_size 32 --accumulation_steps 1 "
|
| 209 |
+
f"--max_seq_len {args.max_seq_len} --use_compile 0 "
|
| 210 |
+
f"--from_weight {shlex.quote(args.save_weight)} "
|
| 211 |
+
f"--save_weight {shlex.quote(args.curriculum_textfix_weight)} "
|
| 212 |
+
"--freeze_backbone none --mode all --use_moe 0 "
|
| 213 |
+
"--grad_clip 1.0 --save_interval 1000 --log_interval 100"
|
| 214 |
+
)
|
| 215 |
+
if phase_id == "phase9_ptbr_t2a_repair":
|
| 216 |
+
return (
|
| 217 |
+
f"cd {repo}/trainer && "
|
| 218 |
+
"CUDA_VISIBLE_DEVICES=0 ../.venv/bin/python -m torch.distributed.run "
|
| 219 |
+
f"--master_port {args.repair_master_port} --nproc_per_node 1 train_sft_omni.py "
|
| 220 |
+
"--learning_rate 5e-5 "
|
| 221 |
+
f"--data_path {shlex.quote('../' + args.t2a_parquet if not args.t2a_parquet.startswith('../') else args.t2a_parquet)} "
|
| 222 |
+
"--epochs 1 --batch_size 16 --accumulation_steps 1 "
|
| 223 |
+
f"--max_seq_len {args.max_seq_len} --use_compile 0 "
|
| 224 |
+
f"--from_weight {shlex.quote(args.curriculum_textfix_weight)} "
|
| 225 |
+
f"--save_weight {shlex.quote(args.curriculum_repair_weight)} "
|
| 226 |
+
"--freeze_backbone all --mode all --use_moe 0 "
|
| 227 |
+
"--grad_clip 1.0 --save_interval 500 --log_interval 50"
|
| 228 |
+
)
|
| 229 |
+
if phase_id == "phase10_ptbr_mixed_sft":
|
| 230 |
+
data_path = (
|
| 231 |
+
f"../{args.curriculum_parquet if not args.curriculum_parquet.startswith('../') else args.curriculum_parquet[3:]},"
|
| 232 |
+
f"../{args.t2a_parquet if not args.t2a_parquet.startswith('../') else args.t2a_parquet[3:]}"
|
| 233 |
+
)
|
| 234 |
+
return (
|
| 235 |
+
f"cd {repo}/trainer && "
|
| 236 |
+
"CUDA_VISIBLE_DEVICES=0 ../.venv/bin/python -m torch.distributed.run "
|
| 237 |
+
f"--master_port {args.mixed_master_port} --nproc_per_node 1 train_sft_omni.py "
|
| 238 |
+
"--learning_rate 1e-5 "
|
| 239 |
+
f"--data_path {shlex.quote(data_path)} "
|
| 240 |
+
"--epochs 1 --batch_size 16 --accumulation_steps 2 "
|
| 241 |
+
f"--max_seq_len {args.max_seq_len} --use_compile 0 "
|
| 242 |
+
f"--from_weight {shlex.quote(args.curriculum_repair_weight)} "
|
| 243 |
+
f"--save_weight {shlex.quote(args.curriculum_mixed_weight)} "
|
| 244 |
+
"--freeze_backbone last1 --mode all --use_moe 0 "
|
| 245 |
+
"--grad_clip 1.0 --save_interval 1000 --log_interval 100"
|
| 246 |
+
)
|
| 247 |
+
if phase_id == "phase11_ptbr_eval_after_mixed":
|
| 248 |
+
return (
|
| 249 |
+
f"cd {repo} && "
|
| 250 |
+
".venv/bin/python tools_ptbr/eval_minimind_talker_ptbr.py --load_from model --save_dir out "
|
| 251 |
+
f"--weight {shlex.quote(args.curriculum_mixed_weight)} "
|
| 252 |
+
"--hidden_size 768 --num_hidden_layers 8 --use_moe 0 "
|
| 253 |
+
"--temperature 0.45 --top-p 0.75 --max-new-tokens 120 "
|
| 254 |
+
"--whisper-model medium "
|
| 255 |
+
f"--output-dir {shlex.quote(args.curriculum_eval_dir)}"
|
| 256 |
+
)
|
| 257 |
+
raise ValueError(f"Unknown phase: {phase_id}")
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def phase_command(args, phase_id):
|
| 261 |
+
return remote_command(args, phase_script(args, phase_id))
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def selected_phases(args):
|
| 265 |
+
if args.phase == "all":
|
| 266 |
+
return [phase["id"] for phase in PHASES]
|
| 267 |
+
return [args.phase]
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def write_plan(args):
|
| 271 |
+
payload = {
|
| 272 |
+
"remote": args.remote,
|
| 273 |
+
"repo": args.repo,
|
| 274 |
+
"phases": [
|
| 275 |
+
{
|
| 276 |
+
**phase,
|
| 277 |
+
"command": shell_join(phase_command(args, phase["id"])),
|
| 278 |
+
}
|
| 279 |
+
for phase in PHASES
|
| 280 |
+
],
|
| 281 |
+
"totals": {
|
| 282 |
+
"estimated_gpu_hours": round(sum(phase["gpu_hours"] for phase in PHASES), 2),
|
| 283 |
+
"wall_clock_prediction": "3-6 h para rodar tudo com 6h PT-BR; 1-2 dias se incluir debug de áudio/escuta.",
|
| 284 |
+
},
|
| 285 |
+
}
|
| 286 |
+
out = Path(args.plan_json)
|
| 287 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 288 |
+
out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 289 |
+
print(json.dumps({"plan_json": str(out), "phases": len(PHASES)}, ensure_ascii=False, indent=2))
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def run_phase(args, phase_id):
|
| 293 |
+
command = phase_command(args, phase_id)
|
| 294 |
+
print(f"\n## {phase_id}")
|
| 295 |
+
print(shell_join(command))
|
| 296 |
+
if args.execute:
|
| 297 |
+
subprocess.run(command, check=True)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def parse_args():
|
| 301 |
+
parser = argparse.ArgumentParser(description="Run or print PT-BR Talker fine-tuning phases.")
|
| 302 |
+
parser.add_argument(
|
| 303 |
+
"phase",
|
| 304 |
+
choices=["all"] + [phase["id"] for phase in PHASES],
|
| 305 |
+
nargs="?",
|
| 306 |
+
default="phase0_preflight",
|
| 307 |
+
)
|
| 308 |
+
parser.add_argument("--execute", action="store_true")
|
| 309 |
+
parser.add_argument("--write-plan", action="store_true")
|
| 310 |
+
parser.add_argument("--plan-json", default="talker_finetune_checklist/phases.json")
|
| 311 |
+
parser.add_argument("--remote", default="root@194.228.55.129")
|
| 312 |
+
parser.add_argument("--ssh-port", type=int, default=33445)
|
| 313 |
+
parser.add_argument("--repo", default="/workspace/minimind-o")
|
| 314 |
+
parser.add_argument("--t2a-parquet", default="data_ptbr/parquet/sft_t2a_ptbr_poc.parquet")
|
| 315 |
+
parser.add_argument(
|
| 316 |
+
"--tucano-merged-model",
|
| 317 |
+
default="out_tucano2/tucano2_1p5b_instruct_lora_gemini20_pass_1811/merged",
|
| 318 |
+
)
|
| 319 |
+
parser.add_argument("--poc-out-dir", default="eval_ptbr/poc_voice_tucano_sft_q4")
|
| 320 |
+
parser.add_argument("--text-mimi-manifest", default="data_ptbr/talker_text_mimi/train_poc.jsonl")
|
| 321 |
+
parser.add_argument("--from-weight", default="sft_omni")
|
| 322 |
+
parser.add_argument("--save-weight", default="sft_omni_ptbr_talker")
|
| 323 |
+
parser.add_argument("--talker-lr", type=float, default=2e-5)
|
| 324 |
+
parser.add_argument("--talker-epochs", type=int, default=2)
|
| 325 |
+
parser.add_argument("--talker-batch-size", type=int, default=16)
|
| 326 |
+
parser.add_argument("--talker-accumulation-steps", type=int, default=2)
|
| 327 |
+
parser.add_argument("--max-seq-len", type=int, default=512)
|
| 328 |
+
parser.add_argument("--master-port", type=int, default=29560)
|
| 329 |
+
parser.add_argument("--minimind-eval-dir", default="eval_ptbr/minimind_talker_ptbr_eval")
|
| 330 |
+
parser.add_argument("--minimind-whisper-csv", default="eval_ptbr/minimind_talker_ptbr_whisper.csv")
|
| 331 |
+
parser.add_argument("--simple-talker-out-dir", default="checkpoints/text_mimi_talker_poc")
|
| 332 |
+
parser.add_argument("--simple-talker-epochs", type=int, default=5)
|
| 333 |
+
parser.add_argument("--simple-talker-batch-size", type=int, default=8)
|
| 334 |
+
parser.add_argument("--curriculum-count", type=int, default=100000)
|
| 335 |
+
parser.add_argument("--curriculum-jsonl", default="data_ptbr/manifests/ptbr_curriculum_100k.jsonl")
|
| 336 |
+
parser.add_argument("--curriculum-parquet", default="data_ptbr/parquet/sft_text_ptbr_curriculum_100k.parquet")
|
| 337 |
+
parser.add_argument("--curriculum-textfix-weight", default="sft_ptbr_curriculum_100k_textfix")
|
| 338 |
+
parser.add_argument("--curriculum-repair-weight", default="sft_ptbr_curriculum_100k_t2a_repair")
|
| 339 |
+
parser.add_argument("--curriculum-mixed-weight", default="sft_ptbr_curriculum_100k_mixed")
|
| 340 |
+
parser.add_argument("--curriculum-eval-dir", default="eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k")
|
| 341 |
+
parser.add_argument("--textfix-master-port", type=int, default=29571)
|
| 342 |
+
parser.add_argument("--repair-master-port", type=int, default=29572)
|
| 343 |
+
parser.add_argument("--mixed-master-port", type=int, default=29573)
|
| 344 |
+
return parser.parse_args()
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def main():
|
| 348 |
+
args = parse_args()
|
| 349 |
+
if args.write_plan:
|
| 350 |
+
write_plan(args)
|
| 351 |
+
for phase_id in selected_phases(args):
|
| 352 |
+
run_phase(args, phase_id)
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
if __name__ == "__main__":
|
| 356 |
+
main()
|
tools/validate_talker_finetune_phases.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import subprocess
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
REMOTE_SCRIPT = r'''
|
| 8 |
+
import csv
|
| 9 |
+
import json
|
| 10 |
+
import re
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
status = {}
|
| 14 |
+
|
| 15 |
+
def item(key, ok, label, detail="", artifacts=None):
|
| 16 |
+
status[key] = {
|
| 17 |
+
"ok": bool(ok),
|
| 18 |
+
"label": label,
|
| 19 |
+
"detail": detail,
|
| 20 |
+
"artifacts": artifacts or [],
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
repo = Path(".")
|
| 24 |
+
|
| 25 |
+
preflight_files = [
|
| 26 |
+
Path("trainer/train_sft_omni.py"),
|
| 27 |
+
Path("data_ptbr/parquet/sft_t2a_ptbr_poc.parquet"),
|
| 28 |
+
Path("out/sft_omni_768.pth"),
|
| 29 |
+
Path("tools_ptbr/eval_generated_with_whisper.py"),
|
| 30 |
+
]
|
| 31 |
+
item(
|
| 32 |
+
"phase0_preflight",
|
| 33 |
+
all(p.exists() for p in preflight_files),
|
| 34 |
+
"Preflight remoto",
|
| 35 |
+
", ".join(f"{p}:{'ok' if p.exists() else 'missing'}" for p in preflight_files),
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
poc_summary = Path("eval_ptbr/poc_voice_tucano_sft_q4_smoke/summary.json")
|
| 39 |
+
if not poc_summary.exists():
|
| 40 |
+
poc_summary = Path("eval_ptbr/poc_voice_tucano_sft_q4/summary.json")
|
| 41 |
+
if poc_summary.exists():
|
| 42 |
+
try:
|
| 43 |
+
summary = json.loads(poc_summary.read_text(encoding="utf-8"))
|
| 44 |
+
except Exception:
|
| 45 |
+
summary = {}
|
| 46 |
+
item("phase1_poc_tucano_q4", True, "POC texto Tucano2 Q4", json.dumps(summary, ensure_ascii=False), [str(poc_summary)])
|
| 47 |
+
else:
|
| 48 |
+
item("phase1_poc_tucano_q4", False, "POC texto Tucano2 Q4", "summary.json não encontrado")
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
import pyarrow.parquet as pq
|
| 52 |
+
t2a = Path("data_ptbr/parquet/sft_t2a_ptbr_poc.parquet")
|
| 53 |
+
text = Path("data_ptbr/parquet/sft_text_tutor_ptbr_50k.parquet")
|
| 54 |
+
manifest = Path("data_ptbr/talker_text_mimi/train_poc.jsonl")
|
| 55 |
+
t2a_rows = pq.read_table(t2a).num_rows if t2a.exists() else 0
|
| 56 |
+
text_rows = pq.read_table(text).num_rows if text.exists() else 0
|
| 57 |
+
manifest_rows = sum(1 for _ in manifest.open("r", encoding="utf-8")) if manifest.exists() else 0
|
| 58 |
+
ok = t2a_rows >= 2000 and manifest_rows >= 1000 and text_rows >= 50000
|
| 59 |
+
detail = f"T2A={t2a_rows} linhas; manifest texto->Mimi={manifest_rows}; SFT textual={text_rows}"
|
| 60 |
+
item("phase2_text_mimi_manifest", ok, "Dataset e manifest", detail, [str(t2a), str(text), str(manifest)])
|
| 61 |
+
except Exception as exc:
|
| 62 |
+
item("phase2_text_mimi_manifest", False, "Dataset e manifest", f"erro ao ler parquet/jsonl: {exc}")
|
| 63 |
+
|
| 64 |
+
ckpt = Path("out/sft_omni_ptbr_talker_768.pth")
|
| 65 |
+
latest = Path("logs/talker_phase3.latest")
|
| 66 |
+
log_path = Path(latest.read_text(encoding="utf-8").strip()) if latest.exists() else None
|
| 67 |
+
log_text = log_path.read_text(encoding="utf-8", errors="replace") if log_path and log_path.exists() else ""
|
| 68 |
+
ok_train = ckpt.exists() and ckpt.stat().st_size > 100_000_000 and "Epoch:[2/2](174/174)" in log_text
|
| 69 |
+
detail_train = f"checkpoint={'ok' if ckpt.exists() else 'missing'}; size_mb={round(ckpt.stat().st_size/1024/1024,1) if ckpt.exists() else 0}; log={log_path or 'missing'}"
|
| 70 |
+
item("phase3_minimind_talker_t2a", ok_train, "Fine-tune Talker", detail_train, [str(ckpt), str(log_path) if log_path else ""])
|
| 71 |
+
|
| 72 |
+
eval_dir = Path("eval_ptbr/minimind_talker_ptbr_eval")
|
| 73 |
+
eval_summary = eval_dir / "summary.json"
|
| 74 |
+
eval_csv = eval_dir / "ptbr_talker_eval.csv"
|
| 75 |
+
wav_files = sorted(eval_dir.glob("*.wav")) if eval_dir.exists() else []
|
| 76 |
+
eval_detail = "avaliação ainda não rodada"
|
| 77 |
+
eval_ok = False
|
| 78 |
+
if eval_summary.exists():
|
| 79 |
+
try:
|
| 80 |
+
summary = json.loads(eval_summary.read_text(encoding="utf-8"))
|
| 81 |
+
except Exception:
|
| 82 |
+
summary = {}
|
| 83 |
+
eval_ok = summary.get("audio_generated", 0) > 0 and summary.get("transcribed", 0) > 0
|
| 84 |
+
eval_detail = json.dumps(summary, ensure_ascii=False)
|
| 85 |
+
item("phase4_minimind_eval_whisper", eval_ok, "Geração de áudio + Whisper", eval_detail, [str(eval_summary), str(eval_csv)] + [str(p) for p in wav_files[:5]])
|
| 86 |
+
|
| 87 |
+
quality_rows = []
|
| 88 |
+
if eval_csv.exists():
|
| 89 |
+
expected = {
|
| 90 |
+
"padaria": ["padaria", "café", "cafe", "atendente"],
|
| 91 |
+
"mercado": ["mercado", "frutas", "onde"],
|
| 92 |
+
"hotel": ["hotel", "recepção", "recepcao"],
|
| 93 |
+
"aula": ["língua", "lingua", "praticar", "todos os dias"],
|
| 94 |
+
"onibus": ["ônibus", "onibus", "centro"],
|
| 95 |
+
}
|
| 96 |
+
english_or_hallucination = {
|
| 97 |
+
"the", "is", "surface", "volcano", "volcanic", "earth", "mythical", "creature",
|
| 98 |
+
"lord", "rings", "estate", "san diego", "parent", "peak", "despair", "auxiliary",
|
| 99 |
+
}
|
| 100 |
+
with eval_csv.open(encoding="utf-8") as f:
|
| 101 |
+
for row in csv.DictReader(f):
|
| 102 |
+
text = f"{row.get('answer_text','')} {row.get('transcript','')}".lower()
|
| 103 |
+
keys = expected.get(row.get("id", ""), [])
|
| 104 |
+
keyword_ok = any(key in text for key in keys)
|
| 105 |
+
english_bad = any(re.search(rf"\b{re.escape(word)}\b", text) for word in english_or_hallucination)
|
| 106 |
+
nonlatin_bad = bool(re.search(r"[\u3040-\u30ff\u3400-\u9fff\uac00-\ud7af�]", text))
|
| 107 |
+
too_long = len(text.split()) > 95
|
| 108 |
+
ok = bool(row.get("generated_audio") == "True" and row.get("transcript") and keyword_ok and not english_bad and not nonlatin_bad and not too_long)
|
| 109 |
+
issues = []
|
| 110 |
+
if not keyword_ok:
|
| 111 |
+
issues.append("baixa relevância")
|
| 112 |
+
if english_bad:
|
| 113 |
+
issues.append("inglês/alucinação")
|
| 114 |
+
if nonlatin_bad:
|
| 115 |
+
issues.append("caracteres não PT-BR")
|
| 116 |
+
if too_long:
|
| 117 |
+
issues.append("resposta longa")
|
| 118 |
+
quality_rows.append((row.get("id", ""), ok, ", ".join(issues) or "ok"))
|
| 119 |
+
quality_ok = bool(quality_rows) and all(ok for _, ok, _ in quality_rows)
|
| 120 |
+
quality_detail = "; ".join(f"{name}:{'ok' if ok else issue}" for name, ok, issue in quality_rows) if quality_rows else "sem CSV de avaliação"
|
| 121 |
+
item(
|
| 122 |
+
"phase4_quality_ptbr",
|
| 123 |
+
quality_ok,
|
| 124 |
+
"Qualidade PT-BR e relevância",
|
| 125 |
+
quality_detail,
|
| 126 |
+
[str(eval_csv)],
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
simple_ckpt = Path("checkpoints/text_mimi_talker_poc/talker.pt")
|
| 130 |
+
smoke_ckpt = Path("/tmp/text_mimi_talker_smoke/talker.pt")
|
| 131 |
+
item(
|
| 132 |
+
"phase5_simple_text_mimi_talker",
|
| 133 |
+
simple_ckpt.exists() or smoke_ckpt.exists(),
|
| 134 |
+
"Protótipo Talker separado",
|
| 135 |
+
f"full={simple_ckpt.exists()}; smoke={smoke_ckpt.exists()}",
|
| 136 |
+
[str(simple_ckpt), str(smoke_ckpt)],
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
report = Path("eval_ptbr/talker_finetune_phase_status.json")
|
| 140 |
+
item("phase6_report", report.exists(), "Relatório final", f"status_json={report.exists()}", [str(report)])
|
| 141 |
+
|
| 142 |
+
curriculum = Path("data_ptbr/parquet/sft_text_ptbr_curriculum_100k.parquet")
|
| 143 |
+
curriculum_jsonl = Path("data_ptbr/manifests/ptbr_curriculum_100k.jsonl")
|
| 144 |
+
try:
|
| 145 |
+
import pyarrow.parquet as pq
|
| 146 |
+
curriculum_rows = pq.read_table(curriculum).num_rows if curriculum.exists() else 0
|
| 147 |
+
except Exception:
|
| 148 |
+
curriculum_rows = 0
|
| 149 |
+
item(
|
| 150 |
+
"phase7_ptbr_curriculum_dataset",
|
| 151 |
+
curriculum_rows >= 100000,
|
| 152 |
+
"Currículo PT-BR 100k",
|
| 153 |
+
f"linhas={curriculum_rows}",
|
| 154 |
+
[str(curriculum), str(curriculum_jsonl)],
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
pipeline_latest = Path("logs/ptbr_curriculum_pipeline.latest")
|
| 158 |
+
pipeline_log_path = Path(pipeline_latest.read_text(encoding="utf-8").strip()) if pipeline_latest.exists() else None
|
| 159 |
+
pipeline_log = pipeline_log_path.read_text(encoding="utf-8", errors="replace") if pipeline_log_path and pipeline_log_path.exists() else ""
|
| 160 |
+
|
| 161 |
+
for key, label, weight, marker in [
|
| 162 |
+
("phase8_ptbr_textfix_sft", "SFT textual PT-BR ampliado", "sft_ptbr_curriculum_100k_textfix", "===== PHASE9"),
|
| 163 |
+
("phase9_ptbr_t2a_repair", "Reparo T2A após textfix", "sft_ptbr_curriculum_100k_t2a_repair", "===== PHASE10"),
|
| 164 |
+
("phase10_ptbr_mixed_sft", "SFT misto texto + áudio", "sft_ptbr_curriculum_100k_mixed", "===== PHASE11"),
|
| 165 |
+
]:
|
| 166 |
+
ckpt_path = Path(f"out/{weight}_768.pth")
|
| 167 |
+
complete = marker in pipeline_log
|
| 168 |
+
item(
|
| 169 |
+
key,
|
| 170 |
+
ckpt_path.exists() and ckpt_path.stat().st_size > 100_000_000 and complete,
|
| 171 |
+
label,
|
| 172 |
+
f"checkpoint={'ok' if ckpt_path.exists() else 'missing'}; complete={complete}; size_mb={round(ckpt_path.stat().st_size/1024/1024,1) if ckpt_path.exists() else 0}",
|
| 173 |
+
[str(ckpt_path), str(pipeline_log_path) if pipeline_log_path else ""],
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
eval2_dir = Path("eval_ptbr/minimind_talker_ptbr_eval_curriculum_100k")
|
| 177 |
+
eval2_summary = eval2_dir / "summary.json"
|
| 178 |
+
eval2_csv = eval2_dir / "ptbr_talker_eval.csv"
|
| 179 |
+
eval2_wavs = sorted(eval2_dir.glob("*.wav")) if eval2_dir.exists() else []
|
| 180 |
+
eval2_ok = False
|
| 181 |
+
eval2_detail = "avaliação ainda não rodada"
|
| 182 |
+
if eval2_summary.exists():
|
| 183 |
+
try:
|
| 184 |
+
summary = json.loads(eval2_summary.read_text(encoding="utf-8"))
|
| 185 |
+
except Exception:
|
| 186 |
+
summary = {}
|
| 187 |
+
eval2_ok = summary.get("audio_generated", 0) > 0 and summary.get("transcribed", 0) > 0
|
| 188 |
+
eval2_detail = json.dumps(summary, ensure_ascii=False)
|
| 189 |
+
item(
|
| 190 |
+
"phase11_ptbr_eval_after_mixed",
|
| 191 |
+
eval2_ok,
|
| 192 |
+
"Reavaliação PT-BR pós-misto",
|
| 193 |
+
eval2_detail,
|
| 194 |
+
[str(eval2_summary), str(eval2_csv)] + [str(p) for p in eval2_wavs[:5]],
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
quality2_rows = []
|
| 198 |
+
if eval2_csv.exists():
|
| 199 |
+
expected = {
|
| 200 |
+
"padaria": ["padaria", "café", "cafe", "atendente"],
|
| 201 |
+
"mercado": ["mercado", "frutas", "onde"],
|
| 202 |
+
"hotel": ["hotel", "recepção", "recepcao"],
|
| 203 |
+
"aula": ["língua", "lingua", "praticar", "todos os dias"],
|
| 204 |
+
"onibus": ["ônibus", "onibus", "centro"],
|
| 205 |
+
}
|
| 206 |
+
bad_words = {
|
| 207 |
+
"the", "is", "surface", "volcano", "volcanic", "earth", "mythical", "creature",
|
| 208 |
+
"lord", "rings", "estate", "san diego", "parent", "peak", "despair", "auxiliary",
|
| 209 |
+
}
|
| 210 |
+
with eval2_csv.open(encoding="utf-8") as f:
|
| 211 |
+
for row in csv.DictReader(f):
|
| 212 |
+
text = f"{row.get('answer_text','')} {row.get('transcript','')}".lower()
|
| 213 |
+
keys = expected.get(row.get("id", ""), [])
|
| 214 |
+
keyword_ok = any(key in text for key in keys)
|
| 215 |
+
english_bad = any(re.search(rf"\b{re.escape(word)}\b", text) for word in bad_words)
|
| 216 |
+
nonlatin_bad = bool(re.search(r"[\u3040-\u30ff\u3400-\u9fff\uac00-\ud7af�]", text))
|
| 217 |
+
too_long = len(text.split()) > 95
|
| 218 |
+
ok = bool(row.get("generated_audio") == "True" and row.get("transcript") and keyword_ok and not english_bad and not nonlatin_bad and not too_long)
|
| 219 |
+
issues = []
|
| 220 |
+
if not keyword_ok:
|
| 221 |
+
issues.append("baixa relevância")
|
| 222 |
+
if english_bad:
|
| 223 |
+
issues.append("inglês/alucinação")
|
| 224 |
+
if nonlatin_bad:
|
| 225 |
+
issues.append("caracteres não PT-BR")
|
| 226 |
+
if too_long:
|
| 227 |
+
issues.append("resposta longa")
|
| 228 |
+
quality2_rows.append((row.get("id", ""), ok, ", ".join(issues) or "ok"))
|
| 229 |
+
quality2_ok = bool(quality2_rows) and all(ok for _, ok, _ in quality2_rows)
|
| 230 |
+
quality2_detail = "; ".join(f"{name}:{'ok' if ok else issue}" for name, ok, issue in quality2_rows) if quality2_rows else "sem CSV de avaliação"
|
| 231 |
+
item("phase11_quality_ptbr_after_mixed", quality2_ok, "Qualidade PT-BR pós-misto", quality2_detail, [str(eval2_csv)])
|
| 232 |
+
|
| 233 |
+
print(json.dumps(status, ensure_ascii=False, indent=2))
|
| 234 |
+
'''
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def parse_args():
|
| 238 |
+
parser = argparse.ArgumentParser(description="Validate remote PT-BR Talker fine-tuning phases.")
|
| 239 |
+
parser.add_argument("--remote", default="root@194.228.55.129")
|
| 240 |
+
parser.add_argument("--ssh-port", type=int, default=33445)
|
| 241 |
+
parser.add_argument("--repo", default="/workspace/minimind-o")
|
| 242 |
+
parser.add_argument("--out-json", default="talker_finetune_checklist/validation_status.json")
|
| 243 |
+
return parser.parse_args()
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def main():
|
| 247 |
+
args = parse_args()
|
| 248 |
+
command = [
|
| 249 |
+
"ssh",
|
| 250 |
+
"-p",
|
| 251 |
+
str(args.ssh_port),
|
| 252 |
+
args.remote,
|
| 253 |
+
f"cd {args.repo} && .venv/bin/python -",
|
| 254 |
+
]
|
| 255 |
+
result = subprocess.run(command, input=REMOTE_SCRIPT, text=True, capture_output=True, check=True)
|
| 256 |
+
status = json.loads(result.stdout)
|
| 257 |
+
out = Path(args.out_json)
|
| 258 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 259 |
+
out.write_text(json.dumps(status, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 260 |
+
print(json.dumps(status, ensure_ascii=False, indent=2))
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
if __name__ == "__main__":
|
| 264 |
+
main()
|