yuanflow-cli 0.1.10 → 0.1.12

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.
Files changed (25) hide show
  1. package/README.md +9 -0
  2. package/package.json +1 -1
  3. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/SKILL.md +91 -0
  4. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/_preview-html-report-styles/index.html +214 -0
  5. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/references/report-shapes.md +139 -0
  6. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/references/theme-system.md +74 -0
  7. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/templates/warm-beige/01-insight-brief.html +98 -0
  8. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/templates/warm-beige/02-data-dashboard.html +86 -0
  9. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/templates/warm-beige/03-deep-report.html +87 -0
  10. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/templates/warm-beige/04-comparison.html +63 -0
  11. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/templates/warm-beige/05-card-collection.html +54 -0
  12. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/templates/warm-beige/06-process-steps.html +57 -0
  13. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/templates/warm-beige/07-timeline.html +55 -0
  14. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/templates/warm-beige/08-action-board.html +58 -0
  15. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/templates/warm-beige/09-knowledge-map.html +72 -0
  16. package/skills/yuanflow-skill/HTML/346/212/245/345/221/212/347/224/237/346/210/220/templates/warm-beige/report-warm-beige.css +258 -0
  17. package/skills/yuanflow-skill/README.md +102 -0
  18. package/skills/yuanflow-skill/SKILL.md +25 -1
  19. package/skills/yuanflow-skill//346/234/254/345/234/260/351/237/263/350/247/206/351/242/221/350/275/254/346/226/207/345/255/227/SKILL.md +148 -0
  20. package/skills/yuanflow-skill//346/234/254/345/234/260/351/237/263/350/247/206/351/242/221/350/275/254/346/226/207/345/255/227/scripts/common/__init__.py +1 -0
  21. package/skills/yuanflow-skill//346/234/254/345/234/260/351/237/263/350/247/206/351/242/221/350/275/254/346/226/207/345/255/227/scripts/common/media.py +46 -0
  22. package/skills/yuanflow-skill//346/234/254/345/234/260/351/237/263/350/247/206/351/242/221/350/275/254/346/226/207/345/255/227/scripts/common/sensevoice.py +82 -0
  23. package/skills/yuanflow-skill//346/234/254/345/234/260/351/237/263/350/247/206/351/242/221/350/275/254/346/226/207/345/255/227/scripts/common/utils.py +62 -0
  24. package/skills/yuanflow-skill//346/234/254/345/234/260/351/237/263/350/247/206/351/242/221/350/275/254/346/226/207/345/255/227/scripts/requirements-transcribe.txt +2 -0
  25. package/skills/yuanflow-skill//346/234/254/345/234/260/351/237/263/350/247/206/351/242/221/350/275/254/346/226/207/345/255/227/scripts/transcribe_media.py +126 -0
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Iterable
7
+
8
+
9
+ VIDEO_SUFFIXES = {".mp4", ".mov", ".mkv", ".avi", ".webm", ".m4v"}
10
+ AUDIO_SUFFIXES = {".wav", ".mp3", ".m4a", ".aac", ".flac", ".ogg", ".opus"}
11
+ MEDIA_SUFFIXES = VIDEO_SUFFIXES | AUDIO_SUFFIXES
12
+ INVALID_FILENAME_RE = re.compile('[<>:"/\\\\|?*\\x00-\\x1F]')
13
+
14
+
15
+ def sanitize_filename(name: str, max_length: int = 120) -> str:
16
+ cleaned = INVALID_FILENAME_RE.sub("_", name).strip()
17
+ cleaned = re.sub(r"\s+", " ", cleaned)
18
+ cleaned = cleaned.rstrip(". ")
19
+ if not cleaned:
20
+ cleaned = "untitled"
21
+ return cleaned[:max_length].rstrip(". ")
22
+
23
+
24
+ def ensure_parent(path: Path) -> Path:
25
+ path.parent.mkdir(parents=True, exist_ok=True)
26
+ return path
27
+
28
+
29
+ def ensure_dir(path: Path) -> Path:
30
+ path.mkdir(parents=True, exist_ok=True)
31
+ return path
32
+
33
+
34
+ def write_json(path: Path, data: object) -> Path:
35
+ ensure_parent(path)
36
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
37
+ return path
38
+
39
+
40
+ def write_text(path: Path, text: str) -> Path:
41
+ ensure_parent(path)
42
+ path.write_text(text, encoding="utf-8")
43
+ return path
44
+
45
+
46
+ def iter_media_files(path: Path, recursive: bool = False) -> Iterable[Path]:
47
+ if path.is_file():
48
+ if path.suffix.lower() in MEDIA_SUFFIXES:
49
+ yield path
50
+ return
51
+ iterator = path.rglob("*") if recursive else path.glob("*")
52
+ for candidate in iterator:
53
+ if candidate.is_file() and candidate.suffix.lower() in MEDIA_SUFFIXES:
54
+ yield candidate
55
+
56
+
57
+ def is_video_file(path: Path) -> bool:
58
+ return path.suffix.lower() in VIDEO_SUFFIXES
59
+
60
+
61
+ def is_audio_file(path: Path) -> bool:
62
+ return path.suffix.lower() in AUDIO_SUFFIXES
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from common.media import extract_audio
7
+ from common.sensevoice import build_model
8
+ from common.sensevoice import clean_transcript
9
+ from common.utils import ensure_dir
10
+ from common.utils import is_audio_file
11
+ from common.utils import is_video_file
12
+ from common.utils import iter_media_files
13
+ from common.utils import sanitize_filename
14
+ from common.utils import write_text
15
+
16
+
17
+ SCRIPT_DIR = Path(__file__).resolve().parent
18
+ DEFAULT_CACHE_ROOT = SCRIPT_DIR / "cache"
19
+ DEFAULT_MODELS_ROOT = SCRIPT_DIR / "models"
20
+
21
+
22
+ def prepare_audio(
23
+ source: Path,
24
+ *,
25
+ audio_cache_dir: Path,
26
+ ffmpeg_bin: str,
27
+ overwrite: bool,
28
+ ) -> tuple[Path, str]:
29
+ if is_audio_file(source):
30
+ return source, "audio"
31
+ if not is_video_file(source):
32
+ raise ValueError(f"不支持的媒体文件类型:{source}")
33
+ output_path = audio_cache_dir / f"{sanitize_filename(source.stem)}.wav"
34
+ extract_audio(
35
+ source,
36
+ output_path,
37
+ ffmpeg_bin=ffmpeg_bin,
38
+ overwrite=overwrite,
39
+ )
40
+ return output_path, "video"
41
+
42
+
43
+ def transcribe_file(
44
+ model,
45
+ audio_file: Path,
46
+ *,
47
+ output_dir: Path,
48
+ language: str,
49
+ batch_size_s: int,
50
+ overwrite: bool,
51
+ ) -> Path:
52
+ output_path = output_dir / f"{sanitize_filename(audio_file.stem)}.txt"
53
+ if output_path.exists() and not overwrite:
54
+ return output_path
55
+ result = model.generate(
56
+ input=str(audio_file),
57
+ cache={},
58
+ language=language,
59
+ use_itn=True,
60
+ batch_size_s=batch_size_s,
61
+ merge_vad=True,
62
+ merge_length_s=15,
63
+ ban_emo_unk=True,
64
+ )
65
+ raw_text = ""
66
+ if result and isinstance(result, list):
67
+ raw_text = str(result[0].get("text", ""))
68
+ return write_text(output_path, clean_transcript(raw_text))
69
+
70
+
71
+ def main() -> None:
72
+ parser = argparse.ArgumentParser(description="本地音视频转文字:视频先抽音频,音频直接转写。")
73
+ parser.add_argument("input_path", type=Path, help="音频文件、视频文件或目录")
74
+ parser.add_argument("--cache-root", type=Path, default=DEFAULT_CACHE_ROOT, help="缓存目录")
75
+ parser.add_argument("--models-root", type=Path, default=DEFAULT_MODELS_ROOT, help="模型目录")
76
+ parser.add_argument("--recursive", action="store_true", help="目录模式下递归扫描")
77
+ parser.add_argument("--overwrite", action="store_true", help="覆盖已有缓存和文本结果")
78
+ parser.add_argument("--ffmpeg-bin", default="ffmpeg", help="ffmpeg 可执行文件名或路径")
79
+ parser.add_argument("--device", default="auto", help="auto、cpu、cuda:0 等")
80
+ parser.add_argument("--language", default="auto", help="zh、en、yue、ja、ko、auto")
81
+ parser.add_argument("--batch-size-s", type=int, default=60, help="动态 batch 秒数")
82
+ args = parser.parse_args()
83
+
84
+ cache_root = args.cache_root.resolve()
85
+ models_root = args.models_root.resolve()
86
+ audio_cache_dir = ensure_dir(cache_root / "audio")
87
+ transcript_dir = ensure_dir(cache_root / "transcripts")
88
+
89
+ media_files = list(iter_media_files(args.input_path.resolve(), recursive=args.recursive))
90
+ if not media_files:
91
+ raise SystemExit("没有找到可处理的音频或视频文件。")
92
+
93
+ try:
94
+ model, downloaded_models = build_model(models_root, device=args.device)
95
+ except ImportError as exc:
96
+ raise SystemExit(
97
+ "缺少 FunASR 或 ModelScope 依赖。请先在 scripts/.venv 中安装 requirements-transcribe.txt。"
98
+ ) from exc
99
+
100
+ print(f"模型目录: {models_root}")
101
+ print(f"缓存目录: {cache_root}")
102
+ print(f"模型首次下载: {'是' if downloaded_models else '否'}")
103
+
104
+ for media_file in media_files:
105
+ audio_file, source_type = prepare_audio(
106
+ media_file,
107
+ audio_cache_dir=audio_cache_dir,
108
+ ffmpeg_bin=args.ffmpeg_bin,
109
+ overwrite=args.overwrite,
110
+ )
111
+ text_path = transcribe_file(
112
+ model,
113
+ audio_file,
114
+ output_dir=transcript_dir,
115
+ language=args.language,
116
+ batch_size_s=max(1, args.batch_size_s),
117
+ overwrite=args.overwrite,
118
+ )
119
+ print(f"输入文件: {media_file}")
120
+ print(f"输入类型: {'视频,已先抽取音频' if source_type == 'video' else '音频'}")
121
+ print(f"音频文件: {audio_file}")
122
+ print(f"转写结果: {text_path}")
123
+
124
+
125
+ if __name__ == "__main__":
126
+ main()