marcosremar2 commited on
Commit
54a18bc
·
verified ·
1 Parent(s): 139dc19

Add PT-BR Talker POC metadata, scripts and evaluation outputs

Browse files
.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()