@horizon_works/banto 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/add_to_config.js +107 -0
- package/gate_core.js +220 -1
- package/law_bridge.js +183 -0
- package/license/client.js +20 -1
- package/package.json +3 -2
- package/server.js +140 -37
- package/payload/compliance/SKILL.md +0 -113
- package/payload/compliance/dict/00_/345/213/225/344/275/234/347/242/272/350/252/215.json +0 -14
- package/payload/compliance/dict/06_house_rules.json +0 -16
- package/payload/compliance/dict/_sample/04_common_keihyo.json +0 -270
- package/payload/compliance/dict/_sample/README.md +0 -72
- package/payload/compliance/lint.js +0 -265
- package/payload/disciplines//347/225/252/351/240/255/343/201/256/344/275/234/346/263/225.md +0 -64
- package/payload/disciplines//350/207/252/345/267/261/345/201/245/350/250/272.md +0 -74
- package/payload/disciplines//351/200/261/346/254/241/343/203/241/343/203/263/343/203/206.md +0 -60
- package/payload/migration/protocol.md +0 -35
- package/payload/skill-maker/SKILL_base_anthropic.md +0 -357
- package/payload/skill-maker/skill-maker-v1.0.md +0 -305
- package/payload/skills/_/343/201/202/343/201/250/343/201/247/350/266/263/343/201/233/343/202/213/343/202/202/343/201/256.md +0 -47
- package/payload/skills/image-gen/SKILL.md +0 -147
- package/payload/skills/image-gen/gen.js +0 -225
- package/payload/skills/remember/SETUP_Google/351/200/243/346/220/272.md +0 -106
- package/payload/skills/remember/SKILL.md +0 -158
- package/payload/skills/remember/handoff.js +0 -149
- package/payload/skills/remember/tasks.js +0 -320
- package/payload/skills/slide-deck/SKILL.md +0 -160
- package/payload/skills/transcribe/README.md +0 -182
- package/payload/skills/transcribe/dict.json +0 -10
- package/payload/skills/transcribe/enroll_speaker.py +0 -76
- package/payload/skills/transcribe/identify_speakers.py +0 -153
- package/payload/skills/transcribe/transcribe.py +0 -126
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"""話者の声紋を登録する(初回のみ・人ごと)
|
|
2
|
-
|
|
3
|
-
使い方:
|
|
4
|
-
python enroll_speaker.py 山田 sample_yamada.m4a
|
|
5
|
-
|
|
6
|
-
音声は「その人だけが話している」10秒〜1分程度のもの。
|
|
7
|
-
過去の録画から切り出す例:
|
|
8
|
-
ffmpeg -i 過去の会議.mp4 -ss 00:05:10 -t 60 sample_yamada.m4a
|
|
9
|
-
|
|
10
|
-
出力:
|
|
11
|
-
voiceprints/山田_voiceprint.npy
|
|
12
|
-
"""
|
|
13
|
-
import subprocess
|
|
14
|
-
import sys
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
|
-
import numpy as np
|
|
18
|
-
|
|
19
|
-
# Windowsの既定はcp932で、日本語を出そうとすると化ける/落ちる。出力をUTF-8に固定する
|
|
20
|
-
try:
|
|
21
|
-
sys.stdout.reconfigure(encoding="utf-8")
|
|
22
|
-
sys.stderr.reconfigure(encoding="utf-8")
|
|
23
|
-
except Exception:
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def load_audio_16k(path):
|
|
29
|
-
"""ffmpegで16kHzモノラルに変換して読み込む(mp4/m4a/mp3/wav対応)"""
|
|
30
|
-
cmd = ["ffmpeg", "-v", "quiet", "-i", str(path),
|
|
31
|
-
"-ac", "1", "-ar", "16000", "-f", "f32le", "-"]
|
|
32
|
-
result = subprocess.run(cmd, capture_output=True)
|
|
33
|
-
if result.returncode != 0 or len(result.stdout) == 0:
|
|
34
|
-
print(f"[error] 音声の読み込みに失敗しました: {path}")
|
|
35
|
-
print(" ffmpegがインストールされているか確認してください(brew install ffmpeg)")
|
|
36
|
-
sys.exit(1)
|
|
37
|
-
return np.frombuffer(result.stdout, dtype=np.float32)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def main():
|
|
41
|
-
if len(sys.argv) < 3:
|
|
42
|
-
print(__doc__)
|
|
43
|
-
sys.exit(1)
|
|
44
|
-
|
|
45
|
-
name, audio_path = sys.argv[1], sys.argv[2]
|
|
46
|
-
if not Path(audio_path).exists():
|
|
47
|
-
print(f"[error] ファイルが見つかりません: {audio_path}")
|
|
48
|
-
sys.exit(1)
|
|
49
|
-
|
|
50
|
-
from resemblyzer import VoiceEncoder, preprocess_wav
|
|
51
|
-
|
|
52
|
-
print(f"[1/3] 音声読み込み: {audio_path}")
|
|
53
|
-
wav = load_audio_16k(audio_path)
|
|
54
|
-
duration = len(wav) / 16000
|
|
55
|
-
print(f" {duration:.0f}秒")
|
|
56
|
-
if duration < 5:
|
|
57
|
-
print("[warn] 5秒未満です。精度が落ちるので10秒以上を推奨します")
|
|
58
|
-
|
|
59
|
-
print(f"[2/3] 声紋を抽出中")
|
|
60
|
-
wav = preprocess_wav(wav, source_sr=16000)
|
|
61
|
-
encoder = VoiceEncoder()
|
|
62
|
-
embed = encoder.embed_utterance(wav)
|
|
63
|
-
|
|
64
|
-
out_dir = Path(__file__).parent / "voiceprints"
|
|
65
|
-
out_dir.mkdir(exist_ok=True)
|
|
66
|
-
out_path = out_dir / f"{name}_voiceprint.npy"
|
|
67
|
-
np.save(str(out_path), embed)
|
|
68
|
-
|
|
69
|
-
print(f"[3/3] 登録完了: {out_path}")
|
|
70
|
-
print(f"\n現在の登録者:")
|
|
71
|
-
for f in sorted(out_dir.glob("*_voiceprint.npy")):
|
|
72
|
-
print(f" - {f.stem.replace('_voiceprint', '')}")
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if __name__ == "__main__":
|
|
76
|
-
main()
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
"""会議音声の話者を識別する
|
|
2
|
-
|
|
3
|
-
使い方:
|
|
4
|
-
python identify_speakers.py 会議録画.mp4 会議録画.json
|
|
5
|
-
|
|
6
|
-
前提:
|
|
7
|
-
- 会議録画.json は transcribe.py の出力
|
|
8
|
-
- voiceprints/ に enroll_speaker.py で参加者の声紋を登録済み
|
|
9
|
-
|
|
10
|
-
出力:
|
|
11
|
-
会議録画_speakers.txt 話者名+タイムスタンプ付き文字起こし
|
|
12
|
-
"""
|
|
13
|
-
import json
|
|
14
|
-
import subprocess
|
|
15
|
-
import sys
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
import numpy as np
|
|
19
|
-
|
|
20
|
-
# Windowsの既定はcp932で、日本語を出そうとすると化ける/落ちる。出力をUTF-8に固定する
|
|
21
|
-
try:
|
|
22
|
-
sys.stdout.reconfigure(encoding="utf-8")
|
|
23
|
-
sys.stderr.reconfigure(encoding="utf-8")
|
|
24
|
-
except Exception:
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# この類似度未満の発話は「不明」にする(0〜1。上げると厳しく、下げると甘く)
|
|
29
|
-
THRESHOLD = 0.60
|
|
30
|
-
|
|
31
|
-
# これより短い発話は声紋照合せず、直前の話者を引き継ぐ(秒)
|
|
32
|
-
MIN_SEG_SEC = 0.8
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def load_audio_16k(path):
|
|
36
|
-
"""ffmpegで16kHzモノラルに変換して読み込む"""
|
|
37
|
-
cmd = ["ffmpeg", "-v", "quiet", "-i", str(path),
|
|
38
|
-
"-ac", "1", "-ar", "16000", "-f", "f32le", "-"]
|
|
39
|
-
result = subprocess.run(cmd, capture_output=True)
|
|
40
|
-
if result.returncode != 0 or len(result.stdout) == 0:
|
|
41
|
-
print(f"[error] 音声の読み込みに失敗しました: {path}")
|
|
42
|
-
sys.exit(1)
|
|
43
|
-
return np.frombuffer(result.stdout, dtype=np.float32)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def load_voiceprints():
|
|
47
|
-
vp_dir = Path(__file__).parent / "voiceprints"
|
|
48
|
-
prints = {}
|
|
49
|
-
if vp_dir.exists():
|
|
50
|
-
for f in sorted(vp_dir.glob("*_voiceprint.npy")):
|
|
51
|
-
name = f.stem.replace("_voiceprint", "")
|
|
52
|
-
prints[name] = np.load(str(f))
|
|
53
|
-
return prints
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def fmt_time(sec):
|
|
57
|
-
sec = int(sec)
|
|
58
|
-
h, m, s = sec // 3600, (sec % 3600) // 60, sec % 60
|
|
59
|
-
return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def main():
|
|
63
|
-
if len(sys.argv) < 3:
|
|
64
|
-
print(__doc__)
|
|
65
|
-
sys.exit(1)
|
|
66
|
-
|
|
67
|
-
audio_path, json_path = Path(sys.argv[1]), Path(sys.argv[2])
|
|
68
|
-
for p in (audio_path, json_path):
|
|
69
|
-
if not p.exists():
|
|
70
|
-
print(f"[error] ファイルが見つかりません: {p}")
|
|
71
|
-
sys.exit(1)
|
|
72
|
-
|
|
73
|
-
voiceprints = load_voiceprints()
|
|
74
|
-
if not voiceprints:
|
|
75
|
-
print("[error] 声紋が1件も登録されていません。先に enroll_speaker.py で登録してください")
|
|
76
|
-
sys.exit(1)
|
|
77
|
-
print(f"登録済み声紋: {', '.join(voiceprints.keys())}")
|
|
78
|
-
|
|
79
|
-
from resemblyzer import VoiceEncoder, preprocess_wav
|
|
80
|
-
|
|
81
|
-
print(f"[1/3] 音声読み込み: {audio_path.name}")
|
|
82
|
-
wav = load_audio_16k(audio_path)
|
|
83
|
-
|
|
84
|
-
data = json.loads(json_path.read_text(encoding="utf-8"))
|
|
85
|
-
segments = data.get("segments", [])
|
|
86
|
-
print(f"[2/3] {len(segments)}セグメントを照合中")
|
|
87
|
-
|
|
88
|
-
encoder = VoiceEncoder()
|
|
89
|
-
names = list(voiceprints.keys())
|
|
90
|
-
vp_matrix = np.stack([voiceprints[n] for n in names])
|
|
91
|
-
|
|
92
|
-
results = []
|
|
93
|
-
prev_speaker = "不明"
|
|
94
|
-
stats = {}
|
|
95
|
-
|
|
96
|
-
for i, seg in enumerate(segments):
|
|
97
|
-
start, end = seg["start"], seg["end"]
|
|
98
|
-
speaker = prev_speaker
|
|
99
|
-
score = None
|
|
100
|
-
|
|
101
|
-
if end - start >= MIN_SEG_SEC:
|
|
102
|
-
chunk = wav[int(start * 16000):int(end * 16000)]
|
|
103
|
-
try:
|
|
104
|
-
processed = preprocess_wav(chunk, source_sr=16000)
|
|
105
|
-
if len(processed) >= 1600:
|
|
106
|
-
embed = encoder.embed_utterance(processed)
|
|
107
|
-
sims = vp_matrix @ embed # 声紋はL2正規化済み=内積がコサイン類似度
|
|
108
|
-
best = int(np.argmax(sims))
|
|
109
|
-
score = float(sims[best])
|
|
110
|
-
speaker = names[best] if score >= THRESHOLD else "不明"
|
|
111
|
-
except Exception:
|
|
112
|
-
pass # 無音・短すぎる区間は直前の話者を引き継ぐ
|
|
113
|
-
|
|
114
|
-
results.append({"speaker": speaker, "start": start, "text": seg["text"], "score": score})
|
|
115
|
-
prev_speaker = speaker
|
|
116
|
-
s = stats.setdefault(speaker, {"count": 0, "scores": []})
|
|
117
|
-
s["count"] += 1
|
|
118
|
-
if score is not None:
|
|
119
|
-
s["scores"].append(score)
|
|
120
|
-
|
|
121
|
-
if (i + 1) % 100 == 0:
|
|
122
|
-
print(f" ... {i + 1}/{len(segments)}")
|
|
123
|
-
|
|
124
|
-
# 連続する同一話者の発話をまとめて出力
|
|
125
|
-
print(f"[3/3] 保存中")
|
|
126
|
-
out_lines = []
|
|
127
|
-
block_speaker, block_start, block_texts = None, None, []
|
|
128
|
-
for r in results:
|
|
129
|
-
if r["speaker"] != block_speaker:
|
|
130
|
-
if block_texts:
|
|
131
|
-
out_lines.append(f"{block_speaker} {fmt_time(block_start)}")
|
|
132
|
-
out_lines.append("".join(block_texts))
|
|
133
|
-
out_lines.append("")
|
|
134
|
-
block_speaker, block_start, block_texts = r["speaker"], r["start"], []
|
|
135
|
-
block_texts.append(r["text"])
|
|
136
|
-
if block_texts:
|
|
137
|
-
out_lines.append(f"{block_speaker} {fmt_time(block_start)}")
|
|
138
|
-
out_lines.append("".join(block_texts))
|
|
139
|
-
|
|
140
|
-
out_path = audio_path.with_name(audio_path.stem + "_speakers.txt")
|
|
141
|
-
out_path.write_text("\n".join(out_lines), encoding="utf-8")
|
|
142
|
-
|
|
143
|
-
print(f"\n完了: {out_path}")
|
|
144
|
-
print(f"\n話者別の内訳:")
|
|
145
|
-
for name, s in sorted(stats.items(), key=lambda x: -x[1]["count"]):
|
|
146
|
-
avg = f"(平均類似度 {np.mean(s['scores']):.2f})" if s["scores"] else ""
|
|
147
|
-
print(f" {name}: {s['count']}セグメント {avg}")
|
|
148
|
-
if stats.get("不明", {}).get("count", 0) > len(results) * 0.3:
|
|
149
|
-
print("\n[hint] 「不明」が3割を超えています。声紋の登録音源を長く・クリアなものに差し替えると改善します")
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if __name__ == "__main__":
|
|
153
|
-
main()
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
"""ローカル文字起こし(faster-whisper)
|
|
2
|
-
|
|
3
|
-
使い方:
|
|
4
|
-
python transcribe.py 会議録画.mp4
|
|
5
|
-
python transcribe.py 会議録画.mp4 --model medium
|
|
6
|
-
|
|
7
|
-
出力(入力ファイルと同じ場所):
|
|
8
|
-
会議録画.txt タイムスタンプ付き文字起こし(人が読む用)
|
|
9
|
-
会議録画.json 詳細データ(identify_speakers.py の入力)
|
|
10
|
-
|
|
11
|
-
dict.json(同じフォルダ)があれば、誤変換を自動補正します。
|
|
12
|
-
"""
|
|
13
|
-
import argparse
|
|
14
|
-
import json
|
|
15
|
-
import sys
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
# Windowsの既定はcp932で、日本語を出そうとすると化ける/落ちる。出力をUTF-8に固定する
|
|
19
|
-
try:
|
|
20
|
-
sys.stdout.reconfigure(encoding="utf-8")
|
|
21
|
-
sys.stderr.reconfigure(encoding="utf-8")
|
|
22
|
-
except Exception:
|
|
23
|
-
pass
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def load_dict():
|
|
28
|
-
"""dict.json の entries を読む(なければ空)"""
|
|
29
|
-
dict_path = Path(__file__).parent / "dict.json"
|
|
30
|
-
if not dict_path.exists():
|
|
31
|
-
return {}
|
|
32
|
-
try:
|
|
33
|
-
data = json.loads(dict_path.read_text(encoding="utf-8"))
|
|
34
|
-
return data.get("entries", {})
|
|
35
|
-
except Exception as e:
|
|
36
|
-
print(f"[warn] dict.json が読めませんでした(補正なしで続行): {e}")
|
|
37
|
-
return {}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def apply_dict(text, entries):
|
|
41
|
-
"""長い表記から先に置換(部分一致の誤爆を減らす)"""
|
|
42
|
-
for wrong in sorted(entries, key=len, reverse=True):
|
|
43
|
-
text = text.replace(wrong, entries[wrong])
|
|
44
|
-
return text
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def fmt_time(sec):
|
|
48
|
-
sec = int(sec)
|
|
49
|
-
h, m, s = sec // 3600, (sec % 3600) // 60, sec % 60
|
|
50
|
-
return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def main():
|
|
54
|
-
parser = argparse.ArgumentParser(description="ローカル文字起こし(faster-whisper)")
|
|
55
|
-
parser.add_argument("input", help="音声・動画ファイル(mp4/m4a/mp3/wav など)")
|
|
56
|
-
parser.add_argument("--model", default="small",
|
|
57
|
-
help="モデルサイズ: tiny/base/small/medium/large-v3(既定: small)")
|
|
58
|
-
args = parser.parse_args()
|
|
59
|
-
|
|
60
|
-
src = Path(args.input)
|
|
61
|
-
if not src.exists():
|
|
62
|
-
print(f"[error] ファイルが見つかりません: {src}")
|
|
63
|
-
sys.exit(1)
|
|
64
|
-
|
|
65
|
-
# ★部品が入っていないときに、Pythonの生のエラーを見せない。
|
|
66
|
-
# 何をすればよいか分からない画面は、そこで人を止めてしまうので。
|
|
67
|
-
try:
|
|
68
|
-
from faster_whisper import WhisperModel
|
|
69
|
-
except ImportError:
|
|
70
|
-
print("")
|
|
71
|
-
print(" 文字起こしの部品(faster-whisper)が入っていません。")
|
|
72
|
-
print("")
|
|
73
|
-
print(" キットのフォルダで、上から順に打ってください。")
|
|
74
|
-
print(" python -m venv .venv")
|
|
75
|
-
print(" .venv" + chr(92) + "Scripts" + chr(92) + "activate ← Windows")
|
|
76
|
-
print(" source .venv/bin/activate ← Mac")
|
|
77
|
-
print(" pip install faster-whisper")
|
|
78
|
-
print("")
|
|
79
|
-
print(" 詳しくは kit/skills/transcribe/README.md を見てください。")
|
|
80
|
-
print(" 番頭に「文字起こしの準備をしたい」と言えば、順番に案内します。")
|
|
81
|
-
print("")
|
|
82
|
-
sys.exit(1)
|
|
83
|
-
|
|
84
|
-
print(f"[1/3] モデル読み込み: {args.model}(初回はダウンロードに数分かかります)")
|
|
85
|
-
model = WhisperModel(args.model, device="cpu", compute_type="int8")
|
|
86
|
-
|
|
87
|
-
print(f"[2/3] 文字起こし中: {src.name}")
|
|
88
|
-
segments, info = model.transcribe(
|
|
89
|
-
str(src),
|
|
90
|
-
language="ja",
|
|
91
|
-
beam_size=5,
|
|
92
|
-
vad_filter=True,
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
entries = load_dict()
|
|
96
|
-
if entries:
|
|
97
|
-
print(f" 辞書補正: {len(entries)}語を適用します")
|
|
98
|
-
|
|
99
|
-
out_txt = src.with_suffix(".txt")
|
|
100
|
-
out_json = src.with_suffix(".json")
|
|
101
|
-
|
|
102
|
-
seg_list = []
|
|
103
|
-
lines = []
|
|
104
|
-
for seg in segments:
|
|
105
|
-
text = apply_dict(seg.text.strip(), entries)
|
|
106
|
-
seg_list.append({"start": round(seg.start, 2), "end": round(seg.end, 2), "text": text})
|
|
107
|
-
lines.append(f"[{fmt_time(seg.start)}] {text}")
|
|
108
|
-
# 進捗表示(5分ごと)
|
|
109
|
-
if len(seg_list) % 100 == 0:
|
|
110
|
-
print(f" ... {fmt_time(seg.start)} まで完了")
|
|
111
|
-
|
|
112
|
-
print(f"[3/3] 保存中")
|
|
113
|
-
out_txt.write_text("\n".join(lines), encoding="utf-8")
|
|
114
|
-
out_json.write_text(
|
|
115
|
-
json.dumps({"duration": round(info.duration, 1), "segments": seg_list},
|
|
116
|
-
ensure_ascii=False, indent=2),
|
|
117
|
-
encoding="utf-8",
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
print(f"\n完了: {len(seg_list)}セグメント / {fmt_time(info.duration)}")
|
|
121
|
-
print(f" {out_txt}")
|
|
122
|
-
print(f" {out_json}")
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if __name__ == "__main__":
|
|
126
|
-
main()
|