#!/usr/bin/env python3 """Luna 1.0 CLI — Двуязычный чат (RU/EN), озвучка текста и генерация звуков. Примеры запуска (внутри распакованной папки luna-1.0): # 1. Спросить на русском и получить текстовый ответ + WAV-озвучку python luna_cli.py "Привет, Луна! Расскажи коротко, что ты умеешь." --out reply_ru.wav # 2. Спросить на английском с космическим эмбиент-фоном python luna_cli.py "Hello Luna! Tell me a fun fact about the Moon." --voice luna_ambient --out reply_en.wav # 3. Озвучить готовый двуязычный текст без генерации ответа (режим TTS) python luna_cli.py --tts "Привет! Меня зовут Luna 1.0. Nice to meet you!" --out bilingual.wav # 4. Сгенерировать звуковой эффект или эмбиент python luna_cli.py --sfx lunar_pad --duration 4.0 --out lunar_pad.wav # 5. Интерактивный голосовой чат в терминале python luna_cli.py --interactive """ from __future__ import annotations import argparse import os from pathlib import Path from transformers import AutoModel DEFAULT_DIR = Path(__file__).resolve().parent def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("prompt", nargs="?", default=None, help="Сообщение для чата или текст для озвучки") parser.add_argument("--model", default=str(DEFAULT_DIR), help="Путь к папке luna-1.0 или HF repo_id") parser.add_argument("--out", default="luna_output.wav", help="Куда сохранить WAV-файл (пусто — без записи)") parser.add_argument("--voice", default="luna_warm", help="Пресет голоса (см. --list-voices)") parser.add_argument("--lang", default="auto", choices=["auto", "ru", "en"], help="Язык (auto / ru / en)") parser.add_argument("--tts", action="store_true", help="Озвучить переданный текст напрямую (без LLM-ответа)") parser.add_argument("--text-only", action="store_true", help="Только текстовый ответ без генерации аудио") parser.add_argument("--sfx", default=None, help="Сгенерировать звуковой эффект (chime, lunar_pad, success, ...)") parser.add_argument("--duration", type=float, default=2.5, help="Длительность эффекта --sfx в секундах") parser.add_argument("--chime", action="store_true", help="Добавить приветственный аккорд Luna перед речью") parser.add_argument("--ambient", default=None, choices=[None, "lunar", "warm", "focus"], help="Фоновый эмбиент") parser.add_argument("--speed", type=float, default=None, help="Скорость речи (например 1.0)") parser.add_argument("--sample-rate", type=int, default=None, help="Частота дискретизации WAV (например 24000)") parser.add_argument("--max-new-tokens", type=int, default=140, help="Максимум новых токенов ответа") parser.add_argument("--temperature", type=float, default=0.7, help="Температура генерации") parser.add_argument("--interactive", action="store_true", help="Интерактивный диалог с записью каждого ответа в WAV") parser.add_argument("--list-voices", action="store_true", help="Показать доступные голосовые пресеты и выйти") return parser.parse_args() def main() -> int: os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") args = parse_args() luna = AutoModel.from_pretrained(args.model, trust_remote_code=True) if args.list_voices: print("Доступные пресеты голосов Luna 1.0:") for name, cfg in luna.list_voices().items(): desc = cfg.get("description_ru") or cfg.get("description_en") or "" print(f" - {name:18s} rate={cfg.get('speaking_rate', 1.0)} {desc}") return 0 if args.sfx: out_path = args.out or f"{args.sfx}.wav" res = luna.create_audio_fx( effect=args.sfx, duration_sec=args.duration, wav_path=out_path, sample_rate=args.sample_rate, ) print(f"[Luna 1.0 SFX] {res.wav_path} ({res.duration_sec:.2f}s, {res.sample_rate} Hz)") return 0 if args.interactive: out_dir = Path("outputs") out_dir.mkdir(exist_ok=True) history: list[dict[str, str]] = [] turn = 1 print("=== Luna 1.0 Interactive Bilingual Chat + Audio (RU / EN) ===") print("Введите сообщение (или 'exit' / 'выход' для завершения).\n") while True: try: user_msg = input("Вы / You > ").strip() except (EOFError, KeyboardInterrupt): print() break if not user_msg or user_msg.lower() in {"exit", "quit", "выход", ":q"}: break wav_file = out_dir / f"turn_{turn:02d}.wav" if args.text_only: reply = luna.chat( user_msg, history=history, language=args.lang, max_new_tokens=args.max_new_tokens, temperature=args.temperature, ) print(f"Luna 1.0 > {reply}\n") else: res = luna.chat_and_speak( user_msg, history=history, language=args.lang, voice=args.voice, wav_path=wav_file, chime=True if args.chime else None, ambient=args.ambient, speaking_rate=args.speed, target_sample_rate=args.sample_rate, max_new_tokens=args.max_new_tokens, temperature=args.temperature, ) reply = res.text print(f"Luna 1.0 > {reply}") print(f" [audio] {res.wav_path} ({res.duration_sec:.2f}s, lang={res.language}, voice={res.voice})\n") history.append({"role": "user", "content": user_msg}) history.append({"role": "assistant", "content": reply}) turn += 1 return 0 prompt = args.prompt or "Привет! Представься коротко на русском и английском." out_wav = args.out if args.out else None if args.tts: res = luna.speak( prompt, language=args.lang, voice=args.voice, wav_path=out_wav, chime=True if args.chime else None, ambient=args.ambient, speaking_rate=args.speed, target_sample_rate=args.sample_rate, ) print(f"[Luna 1.0 TTS] {res.text}") print(f"[Audio] {res.wav_path} ({res.duration_sec:.2f}s, {res.sample_rate} Hz, lang={res.language})") return 0 if args.text_only: reply = luna.chat( prompt, language=args.lang, max_new_tokens=args.max_new_tokens, temperature=args.temperature, ) print(reply) return 0 res = luna.chat_and_speak( prompt, language=args.lang, voice=args.voice, wav_path=out_wav, chime=True if args.chime else None, ambient=args.ambient, speaking_rate=args.speed, target_sample_rate=args.sample_rate, max_new_tokens=args.max_new_tokens, temperature=args.temperature, ) print(f"Luna 1.0: {res.text}") if res.wav_path: print(f"Аудио сохранено: {res.wav_path} ({res.duration_sec:.2f} с, {res.sample_rate} Гц, язык: {res.language})") return 0 if __name__ == "__main__": raise SystemExit(main())