forked from xiaozhi/xiaozhi-esp32
feat: Add locales with OGG sounds (#1057)
* fix jiuchuan build problem * feat: Add locales with OGG sounds * fix building echoear * Support ogg files frame duration <= 60
This commit is contained in:
@@ -4,6 +4,7 @@ import json
|
||||
import os
|
||||
|
||||
HEADER_TEMPLATE = """// Auto-generated language config
|
||||
// Language: {lang_code} with en-US fallback
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
@@ -16,19 +17,58 @@ namespace Lang {{
|
||||
// 语言元数据
|
||||
constexpr const char* CODE = "{lang_code}";
|
||||
|
||||
// 字符串资源
|
||||
// 字符串资源 (en-US as fallback for missing keys)
|
||||
namespace Strings {{
|
||||
{strings}
|
||||
}}
|
||||
|
||||
// 音效资源
|
||||
// 音效资源 (en-US as fallback for missing audio files)
|
||||
namespace Sounds {{
|
||||
{sounds}
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
def generate_header(input_path, output_path):
|
||||
def load_base_language(assets_dir):
|
||||
"""加载 en-US 基准语言数据"""
|
||||
base_lang_path = os.path.join(assets_dir, 'locales', 'en-US', 'language.json')
|
||||
if os.path.exists(base_lang_path):
|
||||
try:
|
||||
with open(base_lang_path, 'r', encoding='utf-8') as f:
|
||||
base_data = json.load(f)
|
||||
print(f"Loaded base language en-US with {len(base_data.get('strings', {}))} strings")
|
||||
return base_data
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Warning: Failed to parse en-US language file: {e}")
|
||||
else:
|
||||
print("Warning: en-US base language file not found, fallback mechanism disabled")
|
||||
return {'strings': {}}
|
||||
|
||||
def get_sound_files(directory):
|
||||
"""获取目录中的音效文件列表"""
|
||||
if not os.path.exists(directory):
|
||||
return []
|
||||
return [f for f in os.listdir(directory) if f.endswith('.ogg')]
|
||||
|
||||
def generate_header(lang_code, output_path):
|
||||
# 从输出路径推导项目结构
|
||||
# output_path 通常是 main/assets/lang_config.h
|
||||
main_dir = os.path.dirname(output_path) # main/assets
|
||||
if os.path.basename(main_dir) == 'assets':
|
||||
main_dir = os.path.dirname(main_dir) # main
|
||||
project_dir = os.path.dirname(main_dir) # 项目根目录
|
||||
assets_dir = os.path.join(main_dir, 'assets')
|
||||
|
||||
# 构建语言JSON文件路径
|
||||
input_path = os.path.join(assets_dir, 'locales', lang_code, 'language.json')
|
||||
|
||||
print(f"Processing language: {lang_code}")
|
||||
print(f"Input file path: {input_path}")
|
||||
print(f"Output file path: {output_path}")
|
||||
|
||||
if not os.path.exists(input_path):
|
||||
raise FileNotFoundError(f"Language file not found: {input_path}")
|
||||
|
||||
with open(input_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
@@ -36,37 +76,88 @@ def generate_header(input_path, output_path):
|
||||
if 'language' not in data or 'strings' not in data:
|
||||
raise ValueError("Invalid JSON structure")
|
||||
|
||||
lang_code = data['language']['type']
|
||||
# 加载 en-US 基准语言数据
|
||||
base_data = load_base_language(assets_dir)
|
||||
|
||||
# 合并字符串:以 en-US 为基准,用户语言覆盖
|
||||
base_strings = base_data.get('strings', {})
|
||||
user_strings = data['strings']
|
||||
merged_strings = base_strings.copy()
|
||||
merged_strings.update(user_strings)
|
||||
|
||||
# 统计信息
|
||||
base_count = len(base_strings)
|
||||
user_count = len(user_strings)
|
||||
total_count = len(merged_strings)
|
||||
fallback_count = total_count - user_count
|
||||
|
||||
print(f"Language {lang_code} string statistics:")
|
||||
print(f" - Base language (en-US): {base_count} strings")
|
||||
print(f" - User language: {user_count} strings")
|
||||
print(f" - Total: {total_count} strings")
|
||||
if fallback_count > 0:
|
||||
print(f" - Fallback to en-US: {fallback_count} strings")
|
||||
|
||||
# 生成字符串常量
|
||||
strings = []
|
||||
sounds = []
|
||||
for key, value in data['strings'].items():
|
||||
for key, value in merged_strings.items():
|
||||
value = value.replace('"', '\\"')
|
||||
strings.append(f' constexpr const char* {key.upper()} = "{value}";')
|
||||
|
||||
# 生成音效常量
|
||||
for file in os.listdir(os.path.dirname(input_path)):
|
||||
if file.endswith('.p3'):
|
||||
base_name = os.path.splitext(file)[0]
|
||||
sounds.append(f'''
|
||||
extern const char p3_{base_name}_start[] asm("_binary_{base_name}_p3_start");
|
||||
extern const char p3_{base_name}_end[] asm("_binary_{base_name}_p3_end");
|
||||
static const std::string_view P3_{base_name.upper()} {{
|
||||
static_cast<const char*>(p3_{base_name}_start),
|
||||
static_cast<size_t>(p3_{base_name}_end - p3_{base_name}_start)
|
||||
# 收集音效文件:以 en-US 为基准,用户语言覆盖
|
||||
current_lang_dir = os.path.join(assets_dir, 'locales', lang_code)
|
||||
base_lang_dir = os.path.join(assets_dir, 'locales', 'en-US')
|
||||
common_dir = os.path.join(assets_dir, 'common')
|
||||
|
||||
# 获取所有可能的音效文件
|
||||
base_sounds = get_sound_files(base_lang_dir)
|
||||
current_sounds = get_sound_files(current_lang_dir)
|
||||
common_sounds = get_sound_files(common_dir)
|
||||
|
||||
# 合并音效文件列表:用户语言覆盖基准语言
|
||||
all_sound_files = set(base_sounds)
|
||||
all_sound_files.update(current_sounds)
|
||||
|
||||
# 音效统计信息
|
||||
base_sound_count = len(base_sounds)
|
||||
user_sound_count = len(current_sounds)
|
||||
common_sound_count = len(common_sounds)
|
||||
sound_fallback_count = len(set(base_sounds) - set(current_sounds))
|
||||
|
||||
print(f"Language {lang_code} sound statistics:")
|
||||
print(f" - Base language (en-US): {base_sound_count} sounds")
|
||||
print(f" - User language: {user_sound_count} sounds")
|
||||
print(f" - Common sounds: {common_sound_count} sounds")
|
||||
if sound_fallback_count > 0:
|
||||
print(f" - Sound fallback to en-US: {sound_fallback_count} sounds")
|
||||
|
||||
# 生成语言特定音效常量
|
||||
for file in sorted(all_sound_files):
|
||||
base_name = os.path.splitext(file)[0]
|
||||
# 优先使用当前语言的音效,如果不存在则回退到 en-US
|
||||
if file in current_sounds:
|
||||
sound_lang = lang_code.replace('-', '_').lower()
|
||||
else:
|
||||
sound_lang = 'en_us'
|
||||
|
||||
sounds.append(f'''
|
||||
extern const char ogg_{base_name}_start[] asm("_binary_{base_name}_ogg_start");
|
||||
extern const char ogg_{base_name}_end[] asm("_binary_{base_name}_ogg_end");
|
||||
static const std::string_view OGG_{base_name.upper()} {{
|
||||
static_cast<const char*>(ogg_{base_name}_start),
|
||||
static_cast<size_t>(ogg_{base_name}_end - ogg_{base_name}_start)
|
||||
}};''')
|
||||
|
||||
# 生成公共音效
|
||||
for file in os.listdir(os.path.join(os.path.dirname(output_path), 'common')):
|
||||
if file.endswith('.p3'):
|
||||
base_name = os.path.splitext(file)[0]
|
||||
sounds.append(f'''
|
||||
extern const char p3_{base_name}_start[] asm("_binary_{base_name}_p3_start");
|
||||
extern const char p3_{base_name}_end[] asm("_binary_{base_name}_p3_end");
|
||||
static const std::string_view P3_{base_name.upper()} {{
|
||||
static_cast<const char*>(p3_{base_name}_start),
|
||||
static_cast<size_t>(p3_{base_name}_end - p3_{base_name}_start)
|
||||
# 生成公共音效常量
|
||||
for file in sorted(common_sounds):
|
||||
base_name = os.path.splitext(file)[0]
|
||||
sounds.append(f'''
|
||||
extern const char ogg_{base_name}_start[] asm("_binary_{base_name}_ogg_start");
|
||||
extern const char ogg_{base_name}_end[] asm("_binary_{base_name}_ogg_end");
|
||||
static const std::string_view OGG_{base_name.upper()} {{
|
||||
static_cast<const char*>(ogg_{base_name}_start),
|
||||
static_cast<size_t>(ogg_{base_name}_end - ogg_{base_name}_start)
|
||||
}};''')
|
||||
|
||||
# 填充模板
|
||||
@@ -83,9 +174,14 @@ def generate_header(input_path, output_path):
|
||||
f.write(content)
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--input", required=True, help="输入JSON文件路径")
|
||||
parser.add_argument("--output", required=True, help="输出头文件路径")
|
||||
parser = argparse.ArgumentParser(description="Generate language configuration header file with en-US fallback")
|
||||
parser.add_argument("--language", required=True, help="Language code (e.g: zh-CN, en-US, ja-JP)")
|
||||
parser.add_argument("--output", required=True, help="Output header file path")
|
||||
args = parser.parse_args()
|
||||
|
||||
generate_header(args.input, args.output)
|
||||
try:
|
||||
generate_header(args.language, args.output)
|
||||
print(f"Successfully generated language config file: {args.output}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
exit(1)
|
||||
Reference in New Issue
Block a user