111
This commit is contained in:
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -5112,7 +5112,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-app"
|
||||
version = "0.1.9"
|
||||
version = "0.1.11"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"async-trait",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-app"
|
||||
version = "0.1.9"
|
||||
version = "0.1.11"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -15,6 +15,10 @@ const APP_COMMANDS: &[&str] = &[
|
||||
"run_nodejs_script",
|
||||
"run_nodejs_script_source",
|
||||
"list_nodejs_scripts",
|
||||
"run_python_script",
|
||||
"run_python_script_source",
|
||||
"list_python_scripts",
|
||||
"stop_python_script",
|
||||
"read_local_file_base64",
|
||||
"write_local_file_base64",
|
||||
"get_media_duration_seconds",
|
||||
|
||||
@@ -17,6 +17,10 @@ permissions = [
|
||||
"allow-run-nodejs-script",
|
||||
"allow-run-nodejs-script-source",
|
||||
"allow-list-nodejs-scripts",
|
||||
"allow-run-python-script",
|
||||
"allow-run-python-script-source",
|
||||
"allow-list-python-scripts",
|
||||
"allow-stop-python-script",
|
||||
"allow-read-local-file-base64",
|
||||
"allow-write-local-file-base64",
|
||||
"allow-get-media-duration-seconds",
|
||||
|
||||
@@ -693,12 +693,13 @@ class AdvancedCoverGenerator:
|
||||
print(f"Converted text position from string to dict: {text_position}", file=sys.stderr)
|
||||
|
||||
# 加载字体
|
||||
font_path = self._get_font_path(font_family)
|
||||
from modules.chinese_font import load_cjk_font
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
except:
|
||||
print(f"Warning: Failed to load font {font_path}, using default", file=sys.stderr)
|
||||
font = ImageFont.load_default()
|
||||
font = load_cjk_font(font_family, font_size, font_weight)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load title font: {e}", file=sys.stderr)
|
||||
font = load_cjk_font("SimHei", font_size, font_weight)
|
||||
|
||||
# ✅ 关键修复:支持多行文字,使用anchor='mm'实现中心对齐
|
||||
# 计算文字中心点位置(百分比转像素)
|
||||
@@ -949,8 +950,8 @@ class AdvancedCoverGenerator:
|
||||
font_size = font_size_from_config
|
||||
# 重新加载字体
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
except:
|
||||
font = load_cjk_font(font_family, font_size, font_weight)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 获取排版参数 - 优先使用 titles.main 中的值,否则使用顶级配置,最后使用默认值
|
||||
@@ -1391,12 +1392,15 @@ class AdvancedCoverGenerator:
|
||||
print(f"Converted subtitle position from string to dict: {subtitle_position}", file=sys.stderr)
|
||||
|
||||
# 加载副标题字体
|
||||
subtitle_font_path = self._get_font_path(subtitle_font_family)
|
||||
from modules.chinese_font import load_cjk_font
|
||||
|
||||
try:
|
||||
subtitle_font = ImageFont.truetype(subtitle_font_path, subtitle_font_size)
|
||||
except:
|
||||
print(f"Warning: Failed to load subtitle font {subtitle_font_path}, using default", file=sys.stderr)
|
||||
subtitle_font = ImageFont.load_default()
|
||||
subtitle_font = load_cjk_font(
|
||||
subtitle_font_family, subtitle_font_size, subtitle_font_weight
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load subtitle font: {e}", file=sys.stderr)
|
||||
subtitle_font = load_cjk_font("SimHei", subtitle_font_size, subtitle_font_weight)
|
||||
|
||||
# 计算副标题中心点位置(百分比转像素)
|
||||
# ✅ 修复旋转坐标问题:有旋转时Y轴需要反转,无旋转时不需要
|
||||
@@ -1442,8 +1446,14 @@ class AdvancedCoverGenerator:
|
||||
subtitle_font_size = subtitle_font_size_from_config
|
||||
# 重新加载字体
|
||||
try:
|
||||
subtitle_font = ImageFont.truetype(subtitle_font_path, subtitle_font_size)
|
||||
except:
|
||||
from modules.chinese_font import load_cjk_font
|
||||
|
||||
subtitle_font = load_cjk_font(
|
||||
subtitle_font_family,
|
||||
subtitle_font_size,
|
||||
subtitle_font_weight,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 获取排版参数 - 优先使用 titles.sub 中的值,否则使用顶级配置,最后使用默认值
|
||||
@@ -1937,13 +1947,13 @@ class AdvancedCoverGenerator:
|
||||
break
|
||||
|
||||
# 如果还是找不到,使用默认字体
|
||||
if not font_path or not os.path.exists(font_path):
|
||||
print(f"Warning: Font file not found for '{font_family}', using fallback", file=sys.stderr)
|
||||
if font_path:
|
||||
print(f" Attempted path: {font_path}", file=sys.stderr)
|
||||
return 'C:\\Windows\\Fonts\\msyh.ttc'
|
||||
try:
|
||||
from modules.chinese_font import resolve_cjk_font_path
|
||||
|
||||
return font_path
|
||||
return resolve_cjk_font_path(font_family, 400)
|
||||
except Exception as e:
|
||||
print(f"Warning: _get_font_path fallback: {e}", file=sys.stderr)
|
||||
return str(Path(os.environ.get("WINDIR", "C:/Windows")) / "Fonts" / "simhei.ttf")
|
||||
|
||||
# ✅ 标准封面尺寸 3:4 比例(与前端 CoverCustomEditor.vue 一致)
|
||||
COVER_WIDTH = 1080
|
||||
|
||||
@@ -115,11 +115,16 @@ def add_text_to_image(img, title, config):
|
||||
|
||||
style = config.get('style', 'default') # default, outline, blur_bg, gradient, split
|
||||
|
||||
# 加载字体
|
||||
# 加载字体(禁止 load_default,否则中文乱码)
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
from modules.chinese_font import load_cjk_font
|
||||
|
||||
font = load_cjk_font(font_family, font_size)
|
||||
except Exception as e:
|
||||
print(f"[cover_generator] font load failed: {e}, retry SimHei", file=sys.stderr)
|
||||
from modules.chinese_font import load_cjk_font
|
||||
|
||||
font = load_cjk_font("SimHei", font_size)
|
||||
|
||||
# 创建绘图对象
|
||||
draw = ImageDraw.Draw(pil_img)
|
||||
|
||||
161
src-tauri/resources/cover-python/modules/chinese_font.py
Normal file
161
src-tauri/resources/cover-python/modules/chinese_font.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""封面中文 PIL / ffmpeg 字体解析(避免 load_default 导致乱码)"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
try:
|
||||
from PIL import ImageFont
|
||||
except ImportError:
|
||||
ImageFont = None # type: ignore
|
||||
|
||||
_FAMILY_ALIASES = {
|
||||
"microsoft yahei": "Microsoft YaHei",
|
||||
"微软雅黑": "Microsoft YaHei",
|
||||
"simhei": "SimHei",
|
||||
"黑体": "SimHei",
|
||||
"simsun": "SimSun",
|
||||
"宋体": "SimSun",
|
||||
"arial": "Arial",
|
||||
"times new roman": "Times New Roman",
|
||||
}
|
||||
|
||||
_WIN_FAMILY_FILES = {
|
||||
"SimHei": ["simhei.ttf"],
|
||||
"Microsoft YaHei": ["msyh.ttc", "msyh.ttf", "msyhbd.ttc"],
|
||||
"SimSun": ["simsun.ttc", "simsunb.ttf"],
|
||||
"Arial": ["arial.ttf", "arialbd.ttf"],
|
||||
"Times New Roman": ["times.ttf", "timesbd.ttf"],
|
||||
}
|
||||
|
||||
|
||||
def _scripts_dir() -> Optional[Path]:
|
||||
env = os.environ.get("AICLIENT_COVER_SCRIPTS_DIR", "").strip()
|
||||
if env:
|
||||
p = Path(env)
|
||||
if p.is_dir():
|
||||
return p
|
||||
# advanced_cover_generator.py 位于 scripts/cover/
|
||||
here = Path(__file__).resolve().parent.parent
|
||||
if (here / "advanced_cover_generator.py").is_file():
|
||||
return here
|
||||
return None
|
||||
|
||||
|
||||
def _windows_fonts_dir() -> Path:
|
||||
windir = os.environ.get("WINDIR", "C:\\Windows")
|
||||
return Path(windir) / "Fonts"
|
||||
|
||||
|
||||
def _normalize_family(font_family: Optional[str]) -> str:
|
||||
raw = (font_family or "Microsoft YaHei").strip()
|
||||
key = raw.lower()
|
||||
return _FAMILY_ALIASES.get(key, raw)
|
||||
|
||||
|
||||
def _candidate_filenames(family: str, weight: int = 400) -> List[str]:
|
||||
names = list(_WIN_FAMILY_FILES.get(family, []))
|
||||
if weight >= 700 and family == "Microsoft YaHei":
|
||||
names = ["msyhbd.ttc", "msyhl.ttc"] + names
|
||||
# 通用回退:.ttf 优先于 .ttc(PIL 对 ttf 更稳)
|
||||
fallback = ["simhei.ttf", "msyh.ttc", "simsun.ttc", "msyh.ttf"]
|
||||
out: List[str] = []
|
||||
for n in names + fallback:
|
||||
if n not in out:
|
||||
out.append(n)
|
||||
return out
|
||||
|
||||
|
||||
def resolve_cjk_font_path(
|
||||
font_family: Optional[str] = None,
|
||||
weight: int = 400,
|
||||
) -> str:
|
||||
"""
|
||||
解析可用于 PIL / ffmpeg 的中文字体文件路径。
|
||||
"""
|
||||
family = _normalize_family(font_family)
|
||||
candidates = _candidate_filenames(family, weight)
|
||||
|
||||
scripts = _scripts_dir()
|
||||
if scripts:
|
||||
fonts_dir = scripts / "fonts"
|
||||
if fonts_dir.is_dir():
|
||||
for name in candidates:
|
||||
p = fonts_dir / name
|
||||
if p.is_file():
|
||||
return str(p.resolve())
|
||||
|
||||
if platform.system() == "Windows":
|
||||
win_fonts = _windows_fonts_dir()
|
||||
for name in candidates:
|
||||
p = win_fonts / name
|
||||
if p.is_file():
|
||||
return str(p.resolve())
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
mac_candidates = [
|
||||
"/System/Library/Fonts/PingFang.ttc",
|
||||
"/System/Library/Fonts/STHeiti Light.ttc",
|
||||
"/Library/Fonts/Arial Unicode.ttf",
|
||||
]
|
||||
for p in mac_candidates:
|
||||
if os.path.isfile(p):
|
||||
return p
|
||||
|
||||
# Linux
|
||||
linux_candidates = [
|
||||
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
||||
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
|
||||
]
|
||||
for p in linux_candidates:
|
||||
if os.path.isfile(p):
|
||||
return p
|
||||
|
||||
# 最后尝试:Windows 黑体
|
||||
if platform.system() == "Windows":
|
||||
simhei = _windows_fonts_dir() / "simhei.ttf"
|
||||
if simhei.is_file():
|
||||
return str(simhei.resolve())
|
||||
|
||||
raise FileNotFoundError(
|
||||
f"未找到中文字体(family={family}),请在 cover/fonts 放置 simhei.ttf 或安装系统字体"
|
||||
)
|
||||
|
||||
|
||||
def load_cjk_font(
|
||||
font_family: Optional[str],
|
||||
size: int,
|
||||
weight: int = 400,
|
||||
) -> "ImageFont.FreeTypeFont":
|
||||
"""加载支持中文的 PIL 字体,禁止回退到 load_default。"""
|
||||
if ImageFont is None:
|
||||
raise RuntimeError("PIL ImageFont 不可用")
|
||||
|
||||
path = resolve_cjk_font_path(font_family, weight)
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
|
||||
if ext == ".ttc":
|
||||
last_err: Optional[Exception] = None
|
||||
for index in range(8):
|
||||
try:
|
||||
font = ImageFont.truetype(path, size, index=index)
|
||||
print(
|
||||
f"[chinese_font] Loaded TTC index={index}: {path} size={size}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return font
|
||||
except OSError as e:
|
||||
last_err = e
|
||||
continue
|
||||
raise OSError(f"无法加载字体集合 {path}: {last_err}")
|
||||
|
||||
font = ImageFont.truetype(path, size)
|
||||
print(f"[chinese_font] Loaded: {path} size={size}", file=sys.stderr)
|
||||
return font
|
||||
@@ -8,6 +8,7 @@ pub mod sound_asr;
|
||||
pub mod video;
|
||||
pub mod fs_util;
|
||||
pub mod nodejs;
|
||||
pub mod pythonscript;
|
||||
pub mod publish;
|
||||
pub mod quickjs;
|
||||
pub mod software_info;
|
||||
|
||||
214
src-tauri/src/commands/pythonscript.rs
Normal file
214
src-tauri/src/commands/pythonscript.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
//! Python 流水线命令:封面等脚本由后端 `/api/v1/python-scripts` 下发,
|
||||
//! 由打包的 `python.exe` 执行。
|
||||
//!
|
||||
//! 前端用法:
|
||||
//! ```ignore
|
||||
//! import { invoke } from "@tauri-apps/api/core";
|
||||
//! import { listen } from "@tauri-apps/api/event";
|
||||
//!
|
||||
//! const off = await listen("python:event", (e) => console.log(e.payload));
|
||||
//! const result = await invoke("run_python_script", {
|
||||
//! scriptName: "cover_generate.py",
|
||||
//! params: { videoPath, titleText, effectStyle },
|
||||
//! });
|
||||
//! off();
|
||||
//! ```
|
||||
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use tauri::{AppHandle, Emitter, Window};
|
||||
use tokio::sync::{mpsc, watch, Mutex};
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::js_runtime::PipelineEvent;
|
||||
use crate::pythonscript;
|
||||
|
||||
const EVENT_NAME: &str = "python:event";
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PythonTaskRegistry {
|
||||
inner: Mutex<HashMap<String, watch::Sender<bool>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum FrontendEvent {
|
||||
Progress {
|
||||
#[serde(rename = "scriptName")]
|
||||
script_name: String,
|
||||
status: String,
|
||||
},
|
||||
Log {
|
||||
#[serde(rename = "scriptName")]
|
||||
script_name: String,
|
||||
level: String,
|
||||
msg: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
fields: Option<Value>,
|
||||
},
|
||||
}
|
||||
|
||||
fn log_python_event(script_name: &str, level: &str, msg: &str, fields: &Option<Value>) {
|
||||
let extra = fields
|
||||
.as_ref()
|
||||
.map(|f| format!(" {f}"))
|
||||
.unwrap_or_default();
|
||||
let line = format!("[{script_name}] {msg}{extra}");
|
||||
match level {
|
||||
"error" => log::error!(target: "pythonscript", "{line}"),
|
||||
"warn" => log::warn!(target: "pythonscript", "{line}"),
|
||||
_ => log::info!(target: "pythonscript", "{line}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spawn_event_pump(
|
||||
app: AppHandle,
|
||||
window: Window,
|
||||
script_name: String,
|
||||
mut rx: mpsc::UnboundedReceiver<PipelineEvent>,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
let win_label = window.label().to_string();
|
||||
tokio::spawn(async move {
|
||||
while let Some(ev) = rx.recv().await {
|
||||
let payload = match &ev {
|
||||
PipelineEvent::Progress(s) => FrontendEvent::Progress {
|
||||
script_name: script_name.clone(),
|
||||
status: s.clone(),
|
||||
},
|
||||
PipelineEvent::Log {
|
||||
level,
|
||||
msg,
|
||||
fields,
|
||||
} => {
|
||||
log_python_event(&script_name, level, msg, fields);
|
||||
FrontendEvent::Log {
|
||||
script_name: script_name.clone(),
|
||||
level: level.clone(),
|
||||
msg: msg.clone(),
|
||||
fields: fields.clone(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let _ = app.emit_to(&win_label, EVENT_NAME, payload);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn finish_event_pump(mut pump: tokio::task::JoinHandle<()>) {
|
||||
if timeout(Duration::from_millis(300), &mut pump).await.is_err() {
|
||||
pump.abort();
|
||||
let _ = pump.await;
|
||||
}
|
||||
}
|
||||
|
||||
/// 按脚本名运行:从后端拉取脚本及本地 import 依赖,spawn python 子进程。
|
||||
#[tauri::command]
|
||||
pub async fn run_python_script(
|
||||
app: AppHandle,
|
||||
window: Window,
|
||||
app_config: tauri::State<'_, AppConfig>,
|
||||
registry: tauri::State<'_, PythonTaskRegistry>,
|
||||
script_name: String,
|
||||
params: Value,
|
||||
) -> Result<Value, String> {
|
||||
let (cancel_tx, cancel_rx) = watch::channel(false);
|
||||
{
|
||||
let mut map = registry.inner.lock().await;
|
||||
if map.contains_key(&script_name) {
|
||||
return Err(format!("脚本正在运行: {script_name}"));
|
||||
}
|
||||
map.insert(script_name.clone(), cancel_tx);
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::unbounded_channel::<PipelineEvent>();
|
||||
let pump = spawn_event_pump(app, window, script_name.clone(), rx);
|
||||
let config_env = app_config.env_for_node().await;
|
||||
|
||||
let result = pythonscript::run_python_script(
|
||||
script_name.clone(),
|
||||
params,
|
||||
tx,
|
||||
config_env,
|
||||
cancel_rx,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
{
|
||||
let mut map = registry.inner.lock().await;
|
||||
map.remove(&script_name);
|
||||
}
|
||||
|
||||
let result = result?;
|
||||
|
||||
finish_event_pump(pump).await;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 调试:直接执行传入的 Python 源码(仍从服务端拉取 import 依赖)。
|
||||
#[tauri::command]
|
||||
pub async fn run_python_script_source(
|
||||
app: AppHandle,
|
||||
window: Window,
|
||||
app_config: tauri::State<'_, AppConfig>,
|
||||
registry: tauri::State<'_, PythonTaskRegistry>,
|
||||
script_source: String,
|
||||
params: Value,
|
||||
) -> Result<Value, String> {
|
||||
let script_key = "<debug-python-source>".to_string();
|
||||
let (cancel_tx, cancel_rx) = watch::channel(false);
|
||||
{
|
||||
let mut map = registry.inner.lock().await;
|
||||
if map.contains_key(&script_key) {
|
||||
return Err("调试 Python 脚本正在运行".to_string());
|
||||
}
|
||||
map.insert(script_key.clone(), cancel_tx);
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::unbounded_channel::<PipelineEvent>();
|
||||
let pump = spawn_event_pump(app, window, script_key.clone(), rx);
|
||||
let config_env = app_config.env_for_node().await;
|
||||
|
||||
let result = pythonscript::run_python_script_source(
|
||||
script_source,
|
||||
params,
|
||||
tx,
|
||||
config_env,
|
||||
cancel_rx,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
{
|
||||
let mut map = registry.inner.lock().await;
|
||||
map.remove(&script_key);
|
||||
}
|
||||
|
||||
let result = result?;
|
||||
|
||||
finish_event_pump(pump).await;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_python_scripts() -> Result<Vec<String>, String> {
|
||||
pythonscript::list_scripts().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_python_script(
|
||||
registry: tauri::State<'_, PythonTaskRegistry>,
|
||||
script_name: String,
|
||||
) -> Result<bool, String> {
|
||||
let sender = {
|
||||
let map = registry.inner.lock().await;
|
||||
map.get(&script_name).cloned()
|
||||
};
|
||||
if let Some(tx) = sender {
|
||||
tx.send(true).map_err(|e| e.to_string())?;
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
//! 封面 Python 运行时与脚本目录定位(对齐 Electron bundled python-runtime)
|
||||
//! 封面 Python 运行时定位(bundled `resources/pythonruntime/python.exe`)
|
||||
//! 封面脚本由服务端下发,不在此目录打包。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn exe_name() -> &'static str {
|
||||
@@ -10,7 +12,43 @@ fn exe_name() -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// bundled `python-runtimebackup` 或系统 / 环境变量 Python。
|
||||
fn pythonruntime_candidates() -> Vec<PathBuf> {
|
||||
let mut out = Vec::new();
|
||||
// 开发源码树优先:target/debug/resources 可能是残缺副本(缺 PIL 等)
|
||||
out.push(
|
||||
PathBuf::from("src-tauri/resources/pythonruntime").join(exe_name()),
|
||||
);
|
||||
out.push(PathBuf::from("resources/pythonruntime").join(exe_name()));
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
out.push(
|
||||
dir.join("resources")
|
||||
.join("pythonruntime")
|
||||
.join(exe_name()),
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `python.exe` 所在目录即 PYTHONHOME(含 Lib/site-packages)。
|
||||
pub fn python_home(python_exe: &Path) -> PathBuf {
|
||||
python_exe
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
}
|
||||
|
||||
fn site_packages_dir(python_exe: &Path) -> PathBuf {
|
||||
python_home(python_exe).join("Lib").join("site-packages")
|
||||
}
|
||||
|
||||
fn runtime_has_pillow(python_exe: &Path) -> bool {
|
||||
let site = site_packages_dir(python_exe);
|
||||
site.join("PIL").is_dir() || site.join("Pillow").is_dir()
|
||||
}
|
||||
|
||||
/// bundled `resources/pythonruntime/python.exe` 或 `AICLIENT_PYTHON_PATH`。
|
||||
pub fn locate_python() -> Option<PathBuf> {
|
||||
if let Ok(p) = std::env::var("AICLIENT_PYTHON_PATH") {
|
||||
let pb = PathBuf::from(&p);
|
||||
@@ -18,35 +56,46 @@ pub fn locate_python() -> Option<PathBuf> {
|
||||
return Some(pb);
|
||||
}
|
||||
}
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
let bundled = dir
|
||||
.join("resources")
|
||||
.join("resources-bundles")
|
||||
.join("python-runtimebackup")
|
||||
.join(exe_name());
|
||||
if bundled.exists() {
|
||||
return Some(bundled);
|
||||
}
|
||||
let cover_only = dir.join("resources").join("cover-python").join(exe_name());
|
||||
if cover_only.exists() {
|
||||
return Some(cover_only);
|
||||
for path in pythonruntime_candidates() {
|
||||
if path.is_file() && runtime_has_pillow(&path) {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
for path in pythonruntime_candidates() {
|
||||
if path.is_file() {
|
||||
return Some(path);
|
||||
}
|
||||
let dev_bundled = PathBuf::from("src-tauri/resources/resources-bundles/python-runtimebackup")
|
||||
.join(exe_name());
|
||||
if dev_bundled.exists() {
|
||||
return Some(dev_bundled);
|
||||
}
|
||||
let dev_cover = PathBuf::from("src-tauri/resources/cover-python").join(exe_name());
|
||||
if dev_cover.exists() {
|
||||
return Some(dev_cover);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 封面脚本目录(`advanced_cover_generator.py` 等)。
|
||||
/// 为 bundled Python 子进程设置 PYTHONHOME / PYTHONPATH(否则找不到 PIL/cv2)。
|
||||
pub fn apply_python_runtime_env(
|
||||
env: &mut HashMap<String, String>,
|
||||
python_exe: &Path,
|
||||
bundle_dir: Option<&Path>,
|
||||
) {
|
||||
let home = python_home(python_exe);
|
||||
env.insert(
|
||||
"PYTHONHOME".into(),
|
||||
home.to_string_lossy().into_owned(),
|
||||
);
|
||||
let site = site_packages_dir(python_exe);
|
||||
let sep = if cfg!(windows) { ";" } else { ":" };
|
||||
let mut paths = vec![site.to_string_lossy().into_owned()];
|
||||
if let Some(dir) = bundle_dir {
|
||||
paths.push(dir.to_string_lossy().into_owned());
|
||||
}
|
||||
env.insert("PYTHONPATH".into(), paths.join(sep));
|
||||
env.remove("PYTHONUSERBASE");
|
||||
env.insert("PYTHONNOUSERSITE".into(), "1".into());
|
||||
env.insert(
|
||||
"AICLIENT_PYTHON_HOME".into(),
|
||||
home.to_string_lossy().into_owned(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 开发期本地脚本目录(发布构建由服务端拉取,不依赖此路径)。
|
||||
pub fn locate_cover_scripts_dir() -> Option<PathBuf> {
|
||||
if let Ok(p) = std::env::var("AICLIENT_COVER_SCRIPTS_DIR") {
|
||||
let pb = PathBuf::from(&p);
|
||||
@@ -54,18 +103,6 @@ pub fn locate_cover_scripts_dir() -> Option<PathBuf> {
|
||||
return Some(pb);
|
||||
}
|
||||
}
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
let bundled = dir.join("resources").join("cover-python");
|
||||
if bundled.join("advanced_cover_generator.py").is_file() {
|
||||
return Some(bundled);
|
||||
}
|
||||
}
|
||||
}
|
||||
let dev = PathBuf::from("src-tauri/resources/cover-python");
|
||||
if dev.join("advanced_cover_generator.py").is_file() {
|
||||
return Some(dev);
|
||||
}
|
||||
let backend = PathBuf::from("../pythonbackend/scripts/cover");
|
||||
if backend.join("advanced_cover_generator.py").is_file() {
|
||||
return backend.canonicalize().ok();
|
||||
@@ -89,10 +126,10 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cover_scripts_dev_path_exists() {
|
||||
let dev = Path::new("src-tauri/resources/cover-python/advanced_cover_generator.py");
|
||||
fn pythonruntime_dev_path() {
|
||||
let dev = PathBuf::from("src-tauri/resources/pythonruntime/python.exe");
|
||||
if dev.is_file() {
|
||||
assert!(locate_cover_scripts_dir().is_some());
|
||||
assert!(locate_python().is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ pub mod ffmpeg;
|
||||
pub mod http;
|
||||
pub mod js_runtime;
|
||||
pub mod nodejs;
|
||||
pub mod pythonscript;
|
||||
pub mod agent_id;
|
||||
pub mod oem_context;
|
||||
pub mod oem_id;
|
||||
@@ -60,6 +61,7 @@ pub fn run() {
|
||||
.manage(video_store::VideoStore::new())
|
||||
.manage(publish_store::PublishStore::new())
|
||||
.manage(commands::nodejs::NodeTaskRegistry::default())
|
||||
.manage(commands::pythonscript::PythonTaskRegistry::default())
|
||||
.setup(|app| {
|
||||
|
||||
let handle = app.handle().clone();
|
||||
@@ -131,6 +133,12 @@ pub fn run() {
|
||||
commands::nodejs::run_nodejs_script_source,
|
||||
commands::nodejs::list_nodejs_scripts,
|
||||
commands::nodejs::stop_nodejs_script,
|
||||
commands::pythonscript::run_python_script,
|
||||
commands::pythonscript::run_python_script_source,
|
||||
commands::pythonscript::list_python_scripts,
|
||||
commands::pythonscript::stop_python_script,
|
||||
|
||||
|
||||
commands::fs_util::read_local_file_base64,
|
||||
commands::fs_util::write_local_file_base64,
|
||||
commands::fs_util::get_media_duration_seconds,
|
||||
|
||||
591
src-tauri/src/pythonscript.rs
Normal file
591
src-tauri/src/pythonscript.rs
Normal file
@@ -0,0 +1,591 @@
|
||||
//! Bundled Python runner:脚本由后端 `GET /api/v1/python-scripts?name=<path>.py` 下发,
|
||||
//! 在 bundled `python.exe` 中执行,协议与 Node 路径对齐(`__EVT__` / `__RESULT__` / `__python_main`)。
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio::sync::watch;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::cover_python;
|
||||
use crate::js_runtime::PipelineEvent;
|
||||
|
||||
static RE_SAFE_SCRIPT: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9._/-]*\.py$").unwrap()
|
||||
});
|
||||
|
||||
static RE_FROM_IMPORT: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(?m)^\s*from\s+([\w.]+)\s+import").unwrap());
|
||||
|
||||
static RE_IMPORT: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(?m)^\s*import\s+([\w.]+)").unwrap());
|
||||
|
||||
static RE_FROM_REL: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(?m)^\s*from\s+\.([\w]+)\s+import").unwrap());
|
||||
|
||||
const INLINE_ENTRY: &str = "__inline_entry__.py";
|
||||
|
||||
#[cfg(windows)]
|
||||
fn hide_console_window(cmd: &mut Command) {
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
cmd.as_std_mut().creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn hide_console_window(_cmd: &mut Command) {}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PythonError {
|
||||
#[error("python.exe 未找到(请检查 src-tauri/resources/pythonruntime/python.exe)")]
|
||||
NotFound,
|
||||
#[error("io: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("拉取脚本失败: {0}")]
|
||||
FetchScript(String),
|
||||
#[error("python 进程非零退出 status={status:?}, stderr={stderr}")]
|
||||
NonZero { status: Option<i32>, stderr: String },
|
||||
#[error("脚本未输出 __RESULT__ 行")]
|
||||
NoResult,
|
||||
#[error("json: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error("任务已取消")]
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
fn normalize_script_name(name: &str) -> Option<String> {
|
||||
let raw = name.trim().replace('\\', "/");
|
||||
if raw.contains("..") {
|
||||
return None;
|
||||
}
|
||||
if RE_SAFE_SCRIPT.is_match(&raw) {
|
||||
Some(raw)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn module_to_rel_path(module: &str) -> Option<String> {
|
||||
if module.starts_with("modules.") {
|
||||
return Some(format!("{}.py", module.replace('.', "/")));
|
||||
}
|
||||
if module == "modules" {
|
||||
return Some("modules/__init__.py".into());
|
||||
}
|
||||
match module {
|
||||
"advanced_cover_generator" | "cover_generator" | "cover_generate" => {
|
||||
Some(format!("{}.py", module))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn relative_import_to_path(current_rel: &str, sub: &str) -> Option<String> {
|
||||
let parent = Path::new(current_rel).parent().unwrap_or(Path::new(""));
|
||||
let joined = parent.join(format!("{}.py", sub));
|
||||
let s = joined.to_string_lossy().replace('\\', "/");
|
||||
Some(s)
|
||||
}
|
||||
|
||||
fn extract_local_deps(source: &str, current_rel: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for cap in RE_FROM_IMPORT.captures_iter(source) {
|
||||
if let Some(m) = cap.get(1) {
|
||||
if let Some(p) = module_to_rel_path(m.as_str()) {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
for cap in RE_IMPORT.captures_iter(source) {
|
||||
if let Some(m) = cap.get(1) {
|
||||
if let Some(p) = module_to_rel_path(m.as_str()) {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
for cap in RE_FROM_REL.captures_iter(source) {
|
||||
if let Some(sub) = cap.get(1) {
|
||||
if let Some(p) = relative_import_to_path(current_rel, sub.as_str()) {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
async fn fetch_python_script_source(script_name: &str) -> Result<String, PythonError> {
|
||||
let q = urlencoding::encode(script_name);
|
||||
let path = format!("/python-scripts?name={q}");
|
||||
let (_status, v) = crate::api_client::request_json("GET", &path, None, HashMap::new())
|
||||
.await
|
||||
.map_err(PythonError::FetchScript)?;
|
||||
if !v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false) {
|
||||
let msg = v
|
||||
.get("message")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("未知错误");
|
||||
return Err(PythonError::FetchScript(msg.to_string()));
|
||||
}
|
||||
let source = v
|
||||
.get("data")
|
||||
.and_then(|d| d.get("source"))
|
||||
.and_then(|s| s.as_str())
|
||||
.ok_or_else(|| PythonError::FetchScript("响应缺少 data.source".into()))?;
|
||||
Ok(source.to_string())
|
||||
}
|
||||
|
||||
async fn fetch_python_script_source_optional(
|
||||
script_name: &str,
|
||||
required: bool,
|
||||
) -> Result<Option<String>, PythonError> {
|
||||
match fetch_python_script_source(script_name).await {
|
||||
Ok(s) => Ok(Some(s)),
|
||||
Err(PythonError::FetchScript(msg))
|
||||
if !required && (msg.contains("不存在") || msg.contains("无效的脚本")) =>
|
||||
{
|
||||
log::warn!(
|
||||
target: "pythonscript",
|
||||
"skip missing optional script dependency: {script_name} ({msg})"
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_script_bundle(entry: &str) -> Result<HashMap<String, String>, PythonError> {
|
||||
let entry_owned = normalize_script_name(entry)
|
||||
.ok_or_else(|| PythonError::FetchScript(format!("无效的入口脚本名: {entry}")))?;
|
||||
|
||||
let mut modules: HashMap<String, String> = HashMap::new();
|
||||
let mut stack = vec![entry_owned.clone()];
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
|
||||
while let Some(name) = stack.pop() {
|
||||
if !seen.insert(name.clone()) {
|
||||
continue;
|
||||
}
|
||||
if modules.contains_key(&name) {
|
||||
continue;
|
||||
}
|
||||
let required = name == entry_owned;
|
||||
let Some(source) = fetch_python_script_source_optional(&name, required).await? else {
|
||||
continue;
|
||||
};
|
||||
for dep in extract_local_deps(&source, &name) {
|
||||
if !seen.contains(&dep) {
|
||||
stack.push(dep);
|
||||
}
|
||||
}
|
||||
modules.insert(name, source);
|
||||
}
|
||||
|
||||
Ok(modules)
|
||||
}
|
||||
|
||||
async fn resolve_script_bundle_for_inline(
|
||||
inline_source: &str,
|
||||
) -> Result<HashMap<String, String>, PythonError> {
|
||||
let mut modules: HashMap<String, String> = HashMap::new();
|
||||
let mut stack: Vec<String> = extract_local_deps(inline_source, INLINE_ENTRY)
|
||||
.into_iter()
|
||||
.collect();
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
|
||||
while let Some(name) = stack.pop() {
|
||||
if !seen.insert(name.clone()) {
|
||||
continue;
|
||||
}
|
||||
if modules.contains_key(&name) {
|
||||
continue;
|
||||
}
|
||||
let Some(source) = fetch_python_script_source_optional(&name, false).await? else {
|
||||
continue;
|
||||
};
|
||||
for dep in extract_local_deps(&source, &name) {
|
||||
if !seen.contains(&dep) {
|
||||
stack.push(dep);
|
||||
}
|
||||
}
|
||||
modules.insert(name, source);
|
||||
}
|
||||
|
||||
modules.insert(INLINE_ENTRY.to_string(), inline_source.to_string());
|
||||
Ok(modules)
|
||||
}
|
||||
|
||||
fn entry_module_name(entry_rel: &str) -> String {
|
||||
Path::new(entry_rel)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("user_entry")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn build_file_runner(entry_rel: &str) -> String {
|
||||
let entry_file = entry_rel.replace('\\', "/");
|
||||
let entry_mod = entry_module_name(entry_rel);
|
||||
format!(
|
||||
r#"
|
||||
# ---- aiclient auto-generated python runner (do not edit) ----
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
def _emit(obj):
|
||||
sys.stderr.write("__EVT__" + json.dumps(obj, ensure_ascii=False) + "\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
class _Native:
|
||||
@staticmethod
|
||||
def emit_progress(status):
|
||||
_emit({{"type": "progress", "status": str(status)}})
|
||||
|
||||
@staticmethod
|
||||
def log(level, msg, fields=None):
|
||||
_emit({{
|
||||
"type": "log",
|
||||
"level": str(level or "info"),
|
||||
"msg": str(msg),
|
||||
"fields": fields,
|
||||
}})
|
||||
|
||||
def main():
|
||||
raw = sys.stdin.read()
|
||||
params = json.loads(raw) if raw.strip() else None
|
||||
bundle_dir = os.getcwd()
|
||||
if bundle_dir not in sys.path:
|
||||
sys.path.insert(0, bundle_dir)
|
||||
entry_file = os.path.join(bundle_dir, "{entry_file}")
|
||||
entry_mod = "{entry_mod}"
|
||||
spec = importlib.util.spec_from_file_location(entry_mod, entry_file)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError("无法加载入口脚本: " + entry_file)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
mod.__dict__["__native"] = _Native
|
||||
sys.modules[entry_mod] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
fn = getattr(mod, "__python_main", None)
|
||||
if fn is None:
|
||||
raise RuntimeError("脚本未定义 __python_main(params)")
|
||||
result = fn(params)
|
||||
sys.stdout.write("__RESULT__" + json.dumps(result, ensure_ascii=False) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
import traceback
|
||||
_emit({{"type": "log", "level": "error", "msg": traceback.format_exc()}})
|
||||
sys.exit(2)
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
async fn write_script_bundle(
|
||||
modules: &HashMap<String, String>,
|
||||
entry: &str,
|
||||
) -> Result<(PathBuf, PathBuf), PythonError> {
|
||||
let tmp_dir = std::env::temp_dir().join("aiclient-python");
|
||||
tokio::fs::create_dir_all(&tmp_dir).await?;
|
||||
let bundle_dir = tmp_dir.join(format!("bundle-{}", Uuid::new_v4()));
|
||||
tokio::fs::create_dir_all(&bundle_dir).await?;
|
||||
|
||||
for (name, source) in modules {
|
||||
let rel = name.replace('\\', "/");
|
||||
let dest = bundle_dir.join(&rel);
|
||||
if let Some(parent) = dest.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
tokio::fs::write(&dest, source).await?;
|
||||
}
|
||||
|
||||
let runner_path = bundle_dir.join("_runner.py");
|
||||
tokio::fs::write(&runner_path, build_file_runner(entry)).await?;
|
||||
|
||||
Ok((bundle_dir, runner_path))
|
||||
}
|
||||
|
||||
pub async fn list_scripts() -> Result<Vec<String>, PythonError> {
|
||||
let (_status, v) =
|
||||
crate::api_client::request_json("GET", "/python-scripts/list", None, HashMap::new())
|
||||
.await
|
||||
.map_err(PythonError::FetchScript)?;
|
||||
if !v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false) {
|
||||
let msg = v
|
||||
.get("message")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("未知错误");
|
||||
return Err(PythonError::FetchScript(msg.to_string()));
|
||||
}
|
||||
let names = v
|
||||
.get("data")
|
||||
.and_then(|d| d.get("names"))
|
||||
.and_then(|n| n.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|x| x.as_str().map(|s| s.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
pub async fn run_python_script(
|
||||
script_name: String,
|
||||
params: Value,
|
||||
events: UnboundedSender<PipelineEvent>,
|
||||
config_env: HashMap<String, String>,
|
||||
mut cancel_rx: watch::Receiver<bool>,
|
||||
) -> Result<Value, PythonError> {
|
||||
let entry = normalize_script_name(&script_name)
|
||||
.ok_or_else(|| PythonError::FetchScript(format!("无效的脚本名称: {script_name}")))?;
|
||||
let modules = resolve_script_bundle(&entry).await?;
|
||||
run_script_bundle(&entry, modules, params, events, config_env, &mut cancel_rx).await
|
||||
}
|
||||
|
||||
pub async fn run_python_script_source(
|
||||
source: String,
|
||||
params: Value,
|
||||
events: UnboundedSender<PipelineEvent>,
|
||||
config_env: HashMap<String, String>,
|
||||
mut cancel_rx: watch::Receiver<bool>,
|
||||
) -> Result<Value, PythonError> {
|
||||
let modules = resolve_script_bundle_for_inline(&source).await?;
|
||||
run_script_bundle(
|
||||
INLINE_ENTRY,
|
||||
modules,
|
||||
params,
|
||||
events,
|
||||
config_env,
|
||||
&mut cancel_rx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn run_script_bundle(
|
||||
entry: &str,
|
||||
modules: HashMap<String, String>,
|
||||
params: Value,
|
||||
events: UnboundedSender<PipelineEvent>,
|
||||
config_env: HashMap<String, String>,
|
||||
cancel_rx: &mut watch::Receiver<bool>,
|
||||
) -> Result<Value, PythonError> {
|
||||
let python = cover_python::locate_python().ok_or(PythonError::NotFound)?;
|
||||
|
||||
let (bundle_dir, script_path) = write_script_bundle(&modules, entry).await?;
|
||||
|
||||
let mut cmd = Command::new(&python);
|
||||
cmd.arg("-u")
|
||||
.arg(&script_path)
|
||||
.kill_on_drop(true)
|
||||
.current_dir(&bundle_dir)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
let mut env_map: HashMap<String, String> = std::env::vars().collect();
|
||||
env_map.insert("PYTHONIOENCODING".into(), "utf-8".into());
|
||||
env_map.insert("PYTHONUTF8".into(), "1".into());
|
||||
cover_python::apply_python_runtime_env(&mut env_map, &python, Some(&bundle_dir));
|
||||
for (k, v) in config_env {
|
||||
env_map.insert(k, v);
|
||||
}
|
||||
if let Some(ffmpeg) = crate::ffmpeg::locate_ffmpeg() {
|
||||
if ffmpeg.exists() {
|
||||
env_map.insert(
|
||||
"AICLIENT_FFMPEG_PATH".into(),
|
||||
ffmpeg.to_string_lossy().into_owned(),
|
||||
);
|
||||
let path_key = "PATH";
|
||||
let new_path = cover_python::prepend_ffmpeg_to_path(
|
||||
env_map.get(path_key).map(|s| s.as_str()).unwrap_or(""),
|
||||
);
|
||||
env_map.insert(path_key.into(), new_path);
|
||||
}
|
||||
}
|
||||
env_map.insert(
|
||||
"AICLIENT_COVER_SCRIPTS_DIR".into(),
|
||||
bundle_dir.to_string_lossy().into_owned(),
|
||||
);
|
||||
env_map.insert(
|
||||
"AICLIENT_PYTHON_PATH".into(),
|
||||
python.to_string_lossy().into_owned(),
|
||||
);
|
||||
for (k, v) in env_map {
|
||||
cmd.env(k, v);
|
||||
}
|
||||
|
||||
hide_console_window(&mut cmd);
|
||||
let mut child = cmd.spawn()?;
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
let json = serde_json::to_vec(¶ms)?;
|
||||
stdin.write_all(&json).await?;
|
||||
stdin.shutdown().await?;
|
||||
}
|
||||
|
||||
let stderr = child.stderr.take().expect("stderr piped");
|
||||
let events_tx = events.clone();
|
||||
let mut stderr_task = tokio::spawn(async move {
|
||||
let mut reader = BufReader::new(stderr).lines();
|
||||
let mut tail = String::new();
|
||||
while let Ok(Some(line)) = reader.next_line().await {
|
||||
if let Some(rest) = line.strip_prefix("__EVT__") {
|
||||
if let Ok(v) = serde_json::from_str::<Value>(rest) {
|
||||
let typ = v.get("type").and_then(|x| x.as_str()).unwrap_or("");
|
||||
match typ {
|
||||
"progress" => {
|
||||
let s = v
|
||||
.get("status")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let _ = events_tx.send(PipelineEvent::Progress(s));
|
||||
}
|
||||
"log" => {
|
||||
let level = v
|
||||
.get("level")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("info")
|
||||
.to_string();
|
||||
let msg = v
|
||||
.get("msg")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let fields = v.get("fields").cloned().filter(|x| !x.is_null());
|
||||
let _ = events_tx.send(PipelineEvent::Log { level, msg, fields });
|
||||
}
|
||||
_ => {
|
||||
let _ = events_tx.send(PipelineEvent::Log {
|
||||
level: "info".into(),
|
||||
msg: rest.to_string(),
|
||||
fields: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !line.is_empty() {
|
||||
let _ = events_tx.send(PipelineEvent::Log {
|
||||
level: "warn".into(),
|
||||
msg: line.clone(),
|
||||
fields: None,
|
||||
});
|
||||
tail.push_str(&line);
|
||||
tail.push('\n');
|
||||
}
|
||||
}
|
||||
tail
|
||||
});
|
||||
|
||||
let stdout = child.stdout.take().expect("stdout piped");
|
||||
let events_tx2 = events.clone();
|
||||
let mut stdout_task = tokio::spawn(async move {
|
||||
let mut reader = BufReader::new(stdout).lines();
|
||||
while let Ok(Some(line)) = reader.next_line().await {
|
||||
if let Some(rest) = line.strip_prefix("__RESULT__") {
|
||||
return Some(rest.to_string());
|
||||
} else if !line.is_empty() {
|
||||
let _ = events_tx2.send(PipelineEvent::Log {
|
||||
level: "info".into(),
|
||||
msg: line,
|
||||
fields: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
let (status, cancelled, result_line, stdout_done) = tokio::select! {
|
||||
result = &mut stdout_task => {
|
||||
let result_line = result.unwrap_or(None);
|
||||
let status = match timeout(Duration::from_secs(120), child.wait()).await {
|
||||
Ok(status) => status?,
|
||||
Err(_) => {
|
||||
let _ = child.start_kill();
|
||||
child.wait().await?
|
||||
}
|
||||
};
|
||||
(status, false, result_line, true)
|
||||
}
|
||||
status = child.wait() => (status?, false, None, false),
|
||||
_ = wait_for_cancel(cancel_rx) => {
|
||||
let _ = child.start_kill();
|
||||
let status = child.wait().await?;
|
||||
(status, true, None, false)
|
||||
}
|
||||
};
|
||||
|
||||
let stderr_tail = match timeout(Duration::from_millis(500), &mut stderr_task).await {
|
||||
Ok(joined) => joined.unwrap_or_default(),
|
||||
Err(_) => {
|
||||
stderr_task.abort();
|
||||
let _ = stderr_task.await;
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
|
||||
let result_line_fallback = if stdout_done {
|
||||
None
|
||||
} else {
|
||||
match timeout(Duration::from_millis(500), &mut stdout_task).await {
|
||||
Ok(joined) => joined.unwrap_or(None),
|
||||
Err(_) => {
|
||||
stdout_task.abort();
|
||||
let _ = stdout_task.await;
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
let result_line = result_line.or(result_line_fallback);
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&bundle_dir).await;
|
||||
|
||||
if cancelled {
|
||||
return Err(PythonError::Cancelled);
|
||||
}
|
||||
|
||||
if let Some(result_str) = result_line {
|
||||
if !status.success() {
|
||||
log::warn!(
|
||||
target: "pythonscript",
|
||||
"python exited non-zero after __RESULT__: status={:?}, stderr_tail={}",
|
||||
status.code(),
|
||||
stderr_tail
|
||||
);
|
||||
}
|
||||
let v: Value = serde_json::from_str(&result_str)?;
|
||||
return Ok(v);
|
||||
}
|
||||
|
||||
if !status.success() {
|
||||
return Err(PythonError::NonZero {
|
||||
status: status.code(),
|
||||
stderr: stderr_tail,
|
||||
});
|
||||
}
|
||||
Err(PythonError::NoResult)
|
||||
}
|
||||
|
||||
async fn wait_for_cancel(cancel_rx: &mut watch::Receiver<bool>) {
|
||||
if *cancel_rx.borrow() {
|
||||
return;
|
||||
}
|
||||
while cancel_rx.changed().await.is_ok() {
|
||||
if *cancel_rx.borrow() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "aiclient",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.11",
|
||||
"identifier": "com.aiclient.desktop",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev --prefix ../aiclient_ui",
|
||||
|
||||
176
templates/cover-template-人像居中.json
Normal file
176
templates/cover-template-人像居中.json
Normal file
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"description": "封面模板导出配置",
|
||||
"timestamp": "2026-06-02T12:13:49.997Z",
|
||||
"coverTemplate": {
|
||||
"id": "v08ezr6v3x-mkdite6i",
|
||||
"name": "人像居中",
|
||||
"config": {
|
||||
"backgroundEnabled": false,
|
||||
"backgroundImagePath": null,
|
||||
"backgroundBlurEnabled": true,
|
||||
"backgroundBlurIntensity": 30,
|
||||
"backgroundSize": 100,
|
||||
"backgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"personImagePath": null,
|
||||
"personSize": 54.99999999999999,
|
||||
"personPosition": {
|
||||
"x": 53.935185185185205,
|
||||
"y": 34.375
|
||||
},
|
||||
"personRotation": 0,
|
||||
"personBorderEnabled": true,
|
||||
"personBorderStyle": "solid",
|
||||
"personBorderColor": "#FFFFFF",
|
||||
"personBorderWidth": 8,
|
||||
"personBorderDashLength": null,
|
||||
"personBorderGapLength": null,
|
||||
"autoSplitText": true,
|
||||
"titleMaxLength": 6,
|
||||
"subtitleMaxLength": 12,
|
||||
"titleText": "托德揭秘芒格",
|
||||
"titleFontFamily": "墨趣古风体",
|
||||
"titleFontSize": 110,
|
||||
"titleFontWeight": 100,
|
||||
"titleColor": "#ffffff",
|
||||
"titleStrokeColor": "#000000",
|
||||
"titleStrokeWidth": 0,
|
||||
"titleShadowColor": "#000000",
|
||||
"titleShadowOffsetX": 0,
|
||||
"titleShadowOffsetY": 0,
|
||||
"titleShadowBlur": 0,
|
||||
"titleShadowEnabled": false,
|
||||
"titleShadowLayers": [
|
||||
{
|
||||
"id": 1765435502861.9124,
|
||||
"offsetX": -2,
|
||||
"offsetY": -2,
|
||||
"blur": 12,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435502861.0488,
|
||||
"offsetX": 2,
|
||||
"offsetY": 2,
|
||||
"blur": 16,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435502861.4207,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"blur": 14,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"titlePosition": {
|
||||
"x": 39.814814814814795,
|
||||
"y": 69.40972222222223
|
||||
},
|
||||
"titleRotation": 0,
|
||||
"titleDirection": "horizontal",
|
||||
"titleCharSpacing": 12,
|
||||
"titleLineSpacing": 118,
|
||||
"titleMaxCharsPerLine": 10,
|
||||
"titleBackgroundEnabled": false,
|
||||
"titleBackgroundColor": "#000000",
|
||||
"titleBackgroundOpacity": 70,
|
||||
"titleBackgroundShape": "rectangle",
|
||||
"titleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"titleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 30
|
||||
},
|
||||
"titleBackgroundPoints": [],
|
||||
"titleBackgroundRadius": 10,
|
||||
"titleBackgroundRotation": 0,
|
||||
"subtitleText": "投资稳的原因",
|
||||
"subtitleFontFamily": "庞门正道标题体免费版",
|
||||
"subtitleFontSize": 117,
|
||||
"subtitleFontWeight": 500,
|
||||
"subtitleColor": "#eaf273",
|
||||
"subtitleStrokeColor": "#000000",
|
||||
"subtitleStrokeWidth": 0,
|
||||
"subtitleShadowColor": "#000000",
|
||||
"subtitleShadowOffsetX": 0,
|
||||
"subtitleShadowOffsetY": 0,
|
||||
"subtitleShadowBlur": 0,
|
||||
"subtitleShadowEnabled": false,
|
||||
"subtitleShadowLayers": [
|
||||
{
|
||||
"id": 1765435591571.2886,
|
||||
"offsetX": -2,
|
||||
"offsetY": -2,
|
||||
"blur": 2,
|
||||
"color": "#FFFFFF",
|
||||
"opacity": 60,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435591571.856,
|
||||
"offsetX": 2,
|
||||
"offsetY": 2,
|
||||
"blur": 0,
|
||||
"color": "#000000",
|
||||
"opacity": 65,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435591571.3809,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"blur": 4,
|
||||
"color": "#000000",
|
||||
"opacity": 50,
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"subtitlePosition": {
|
||||
"x": 58.796296296296305,
|
||||
"y": 77.84722222222221
|
||||
},
|
||||
"subtitleRotation": 0,
|
||||
"subtitleDirection": "horizontal",
|
||||
"subtitleCharSpacing": 12,
|
||||
"subtitleLineSpacing": 72,
|
||||
"subtitleMaxCharsPerLine": 15,
|
||||
"subtitleBackgroundEnabled": false,
|
||||
"subtitleBackgroundColor": "#000000",
|
||||
"subtitleBackgroundOpacity": 70,
|
||||
"subtitleBackgroundShape": "rectangle",
|
||||
"subtitleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"subtitleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 60
|
||||
},
|
||||
"subtitleBackgroundPoints": [],
|
||||
"subtitleBackgroundRadius": 10,
|
||||
"subtitleBackgroundRotation": 0,
|
||||
"maskEnabled": false,
|
||||
"maskImagePath": null,
|
||||
"maskSize": 100,
|
||||
"maskPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"maskColor": null,
|
||||
"maskOpacity": 100,
|
||||
"maskShape": "rectangle"
|
||||
}
|
||||
}
|
||||
}
|
||||
164
templates/cover-template-大四字报.json
Normal file
164
templates/cover-template-大四字报.json
Normal file
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"description": "封面模板导出配置",
|
||||
"timestamp": "2026-06-02T12:13:38.566Z",
|
||||
"coverTemplate": {
|
||||
"id": "705fa010-5bf3-4041-b624-0fd364bea5ad",
|
||||
"name": "大四字报",
|
||||
"config": {
|
||||
"backgroundEnabled": true,
|
||||
"backgroundImagePath": null,
|
||||
"backgroundBlurEnabled": false,
|
||||
"backgroundBlurIntensity": 30,
|
||||
"backgroundSize": 100,
|
||||
"backgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"personImagePath": null,
|
||||
"personSize": 33.125,
|
||||
"personPosition": {
|
||||
"x": 22.68518518518519,
|
||||
"y": 78.125
|
||||
},
|
||||
"personRotation": 0,
|
||||
"personBorderEnabled": false,
|
||||
"personBorderStyle": "solid",
|
||||
"personBorderColor": "#FFFFFF",
|
||||
"personBorderWidth": 8,
|
||||
"personBorderDashLength": null,
|
||||
"personBorderGapLength": null,
|
||||
"autoSplitText": true,
|
||||
"titleMaxLength": 4,
|
||||
"subtitleMaxLength": 30,
|
||||
"titleText": "礼貌回归",
|
||||
"titleFontFamily": "USMCCyuanjiantecu",
|
||||
"titleFontSize": 375,
|
||||
"titleFontWeight": 400,
|
||||
"titleColor": "#e6ca19",
|
||||
"titleStrokeColor": "#000000",
|
||||
"titleStrokeWidth": 0,
|
||||
"titleShadowColor": "#000000",
|
||||
"titleShadowOffsetX": 0,
|
||||
"titleShadowOffsetY": 0,
|
||||
"titleShadowBlur": 0,
|
||||
"titleShadowEnabled": false,
|
||||
"titleShadowLayers": [
|
||||
{
|
||||
"id": 1765355385133.3418,
|
||||
"offsetX": 20,
|
||||
"offsetY": 20,
|
||||
"blur": 30,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765355385133.7036,
|
||||
"offsetX": -20,
|
||||
"offsetY": -20,
|
||||
"blur": 30,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"titlePosition": {
|
||||
"x": 52.54629629629629,
|
||||
"y": 48.40277777777778
|
||||
},
|
||||
"titleRotation": 0,
|
||||
"titleDirection": "horizontal",
|
||||
"titleCharSpacing": 58,
|
||||
"titleLineSpacing": 150,
|
||||
"titleMaxCharsPerLine": 2,
|
||||
"titleBackgroundEnabled": false,
|
||||
"titleBackgroundColor": "#000000",
|
||||
"titleBackgroundOpacity": 70,
|
||||
"titleBackgroundShape": "polygon",
|
||||
"titleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"titleBackgroundPosition": {
|
||||
"x": 95.83333333333333,
|
||||
"y": 68.88888888888889
|
||||
},
|
||||
"titleBackgroundPoints": [
|
||||
{
|
||||
"x": -330.24691358024694,
|
||||
"y": -15.440538194444414
|
||||
},
|
||||
{
|
||||
"x": 40,
|
||||
"y": -40
|
||||
},
|
||||
{
|
||||
"x": 40,
|
||||
"y": 40
|
||||
},
|
||||
{
|
||||
"x": -40,
|
||||
"y": 40
|
||||
},
|
||||
{
|
||||
"x": -114.9691358024691,
|
||||
"y": 36.642795138888864
|
||||
},
|
||||
{
|
||||
"x": -96.45061728395058,
|
||||
"y": -3.2877604166666865
|
||||
}
|
||||
],
|
||||
"titleBackgroundRadius": 10,
|
||||
"titleBackgroundRotation": 0,
|
||||
"subtitleText": "行动亲爱的观众朋友们",
|
||||
"subtitleFontFamily": "USMCCyuanjiantecu",
|
||||
"subtitleFontSize": 122,
|
||||
"subtitleFontWeight": 500,
|
||||
"subtitleColor": "#e37a16",
|
||||
"subtitleStrokeColor": "#000000",
|
||||
"subtitleStrokeWidth": 5,
|
||||
"subtitleShadowColor": "#ff0000",
|
||||
"subtitleShadowOffsetX": 41,
|
||||
"subtitleShadowOffsetY": 50,
|
||||
"subtitleShadowBlur": 13,
|
||||
"subtitleShadowEnabled": false,
|
||||
"subtitleShadowLayers": [],
|
||||
"subtitlePosition": {
|
||||
"x": 51.15740740740741,
|
||||
"y": 83.75
|
||||
},
|
||||
"subtitleRotation": 0,
|
||||
"subtitleDirection": "horizontal",
|
||||
"subtitleCharSpacing": 0,
|
||||
"subtitleLineSpacing": 72,
|
||||
"subtitleMaxCharsPerLine": 8,
|
||||
"subtitleBackgroundEnabled": false,
|
||||
"subtitleBackgroundColor": "#000000",
|
||||
"subtitleBackgroundOpacity": 70,
|
||||
"subtitleBackgroundShape": "rectangle",
|
||||
"subtitleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"subtitleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 60
|
||||
},
|
||||
"subtitleBackgroundPoints": [],
|
||||
"subtitleBackgroundRadius": 10,
|
||||
"subtitleBackgroundRotation": 0,
|
||||
"maskEnabled": false,
|
||||
"maskImagePath": null,
|
||||
"maskSize": 100,
|
||||
"maskPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"maskColor": null,
|
||||
"maskOpacity": 100,
|
||||
"maskShape": "rectangle"
|
||||
}
|
||||
}
|
||||
}
|
||||
137
templates/cover-template-斜黄白.json
Normal file
137
templates/cover-template-斜黄白.json
Normal file
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"description": "封面模板导出配置",
|
||||
"timestamp": "2026-06-02T12:12:07.312Z",
|
||||
"coverTemplate": {
|
||||
"id": "8f17c467-c69c-45b2-8011-94a6c3071d16",
|
||||
"name": "斜黄白",
|
||||
"config": {
|
||||
"backgroundEnabled": false,
|
||||
"backgroundImagePath": null,
|
||||
"backgroundBlurEnabled": false,
|
||||
"backgroundBlurIntensity": 10,
|
||||
"backgroundSize": 100,
|
||||
"backgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"personImagePath": null,
|
||||
"personSize": 80,
|
||||
"personPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"personRotation": 0,
|
||||
"personBorderEnabled": false,
|
||||
"personBorderStyle": "solid",
|
||||
"personBorderColor": "#FFFFFF",
|
||||
"personBorderWidth": 8,
|
||||
"personBorderDashLength": null,
|
||||
"personBorderGapLength": null,
|
||||
"autoSplitText": true,
|
||||
"titleMaxLength": 4,
|
||||
"subtitleMaxLength": 10,
|
||||
"titleText": "礼貌回归",
|
||||
"titleFontFamily": "斗鱼追光体2.0",
|
||||
"titleFontSize": 120,
|
||||
"titleFontWeight": 700,
|
||||
"titleColor": "#f5d400",
|
||||
"titleStrokeColor": "#000000",
|
||||
"titleStrokeWidth": 3,
|
||||
"titleShadowColor": "#000000",
|
||||
"titleShadowOffsetX": 0,
|
||||
"titleShadowOffsetY": 0,
|
||||
"titleShadowBlur": 0,
|
||||
"titleShadowEnabled": false,
|
||||
"titleShadowLayers": [],
|
||||
"titlePosition": {
|
||||
"x": 42.59259259259258,
|
||||
"y": 56.909722222222214
|
||||
},
|
||||
"titleRotation": 338,
|
||||
"titleDirection": "horizontal",
|
||||
"titleCharSpacing": 24,
|
||||
"titleLineSpacing": 144,
|
||||
"titleMaxCharsPerLine": 10,
|
||||
"titleBackgroundEnabled": true,
|
||||
"titleBackgroundColor": "#f5f5f5",
|
||||
"titleBackgroundOpacity": 25,
|
||||
"titleBackgroundShape": "polygon",
|
||||
"titleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"titleBackgroundPosition": {
|
||||
"x": 44.21296296296296,
|
||||
"y": 32.951388888888886
|
||||
},
|
||||
"titleBackgroundPoints": [
|
||||
{
|
||||
"x": 226.08024691358025,
|
||||
"y": -79.67664930555557
|
||||
},
|
||||
{
|
||||
"x": 250.7716049382716,
|
||||
"y": 218.93446180555554
|
||||
},
|
||||
{
|
||||
"x": -260.8024691358025,
|
||||
"y": 691.1566840277777
|
||||
},
|
||||
{
|
||||
"x": -290.12345679012344,
|
||||
"y": 366.50390624999994
|
||||
}
|
||||
],
|
||||
"titleBackgroundRadius": 10,
|
||||
"titleBackgroundRotation": 0,
|
||||
"subtitleText": "行动亲爱的观众朋友们",
|
||||
"subtitleFontFamily": "斗鱼追光体2.0",
|
||||
"subtitleFontSize": 82,
|
||||
"subtitleFontWeight": 500,
|
||||
"subtitleColor": "#FFFFFF",
|
||||
"subtitleStrokeColor": "#000000",
|
||||
"subtitleStrokeWidth": 1,
|
||||
"subtitleShadowColor": "#000000",
|
||||
"subtitleShadowOffsetX": 0,
|
||||
"subtitleShadowOffsetY": 0,
|
||||
"subtitleShadowBlur": 0,
|
||||
"subtitleShadowEnabled": false,
|
||||
"subtitleShadowLayers": [],
|
||||
"subtitlePosition": {
|
||||
"x": 45.37037037037037,
|
||||
"y": 66.5625
|
||||
},
|
||||
"subtitleRotation": 338,
|
||||
"subtitleDirection": "horizontal",
|
||||
"subtitleCharSpacing": 23,
|
||||
"subtitleLineSpacing": 72,
|
||||
"subtitleMaxCharsPerLine": 17,
|
||||
"subtitleBackgroundEnabled": false,
|
||||
"subtitleBackgroundColor": "#000000",
|
||||
"subtitleBackgroundOpacity": 70,
|
||||
"subtitleBackgroundShape": "rectangle",
|
||||
"subtitleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"subtitleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 60
|
||||
},
|
||||
"subtitleBackgroundPoints": [],
|
||||
"subtitleBackgroundRadius": 10,
|
||||
"subtitleBackgroundRotation": 0,
|
||||
"maskEnabled": false,
|
||||
"maskImagePath": null,
|
||||
"maskSize": 100,
|
||||
"maskPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"maskColor": null,
|
||||
"maskOpacity": 100,
|
||||
"maskShape": "rectangle"
|
||||
}
|
||||
}
|
||||
}
|
||||
148
templates/cover-template-粉红虚白.json
Normal file
148
templates/cover-template-粉红虚白.json
Normal file
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"description": "封面模板导出配置",
|
||||
"timestamp": "2026-06-02T12:13:56.933Z",
|
||||
"coverTemplate": {
|
||||
"id": "43t1uykj8zg-mkdite6i",
|
||||
"name": "粉红虚白",
|
||||
"config": {
|
||||
"backgroundEnabled": false,
|
||||
"backgroundImagePath": null,
|
||||
"backgroundBlurEnabled": true,
|
||||
"backgroundBlurIntensity": 50,
|
||||
"backgroundSize": 100,
|
||||
"backgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"personImagePath": null,
|
||||
"personSize": 81.7361111111111,
|
||||
"personPosition": {
|
||||
"x": 49.768518518518526,
|
||||
"y": 60.41666666666666
|
||||
},
|
||||
"personRotation": 0,
|
||||
"personBorderEnabled": true,
|
||||
"personBorderStyle": "dashed",
|
||||
"personBorderColor": "#ffffff",
|
||||
"personBorderWidth": 8,
|
||||
"personBorderDashLength": null,
|
||||
"personBorderGapLength": null,
|
||||
"autoSplitText": true,
|
||||
"titleMaxLength": 4,
|
||||
"subtitleMaxLength": 11,
|
||||
"titleText": "长期持有",
|
||||
"titleFontFamily": "庞门正道标题体免费版",
|
||||
"titleFontSize": 165,
|
||||
"titleFontWeight": 100,
|
||||
"titleColor": "#e8c4df",
|
||||
"titleStrokeColor": "#e147c0",
|
||||
"titleStrokeWidth": 6,
|
||||
"titleShadowColor": "#000000",
|
||||
"titleShadowOffsetX": 0,
|
||||
"titleShadowOffsetY": 0,
|
||||
"titleShadowBlur": 0,
|
||||
"titleShadowEnabled": false,
|
||||
"titleShadowLayers": [],
|
||||
"titlePosition": {
|
||||
"x": 36.80555555555553,
|
||||
"y": 11.944444444444452
|
||||
},
|
||||
"titleRotation": 0,
|
||||
"titleDirection": "horizontal",
|
||||
"titleCharSpacing": 11,
|
||||
"titleLineSpacing": 118,
|
||||
"titleMaxCharsPerLine": 10,
|
||||
"titleBackgroundEnabled": false,
|
||||
"titleBackgroundColor": "#000000",
|
||||
"titleBackgroundOpacity": 70,
|
||||
"titleBackgroundShape": "rectangle",
|
||||
"titleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"titleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 30
|
||||
},
|
||||
"titleBackgroundPoints": [],
|
||||
"titleBackgroundRadius": 10,
|
||||
"titleBackgroundRotation": 0,
|
||||
"subtitleText": "才是真省心又赚钱",
|
||||
"subtitleFontFamily": "方正粗黑宋简体",
|
||||
"subtitleFontSize": 117,
|
||||
"subtitleFontWeight": 500,
|
||||
"subtitleColor": "#FFFFFF",
|
||||
"subtitleStrokeColor": "#ef62e3",
|
||||
"subtitleStrokeWidth": 5,
|
||||
"subtitleShadowColor": "#000000",
|
||||
"subtitleShadowOffsetX": 0,
|
||||
"subtitleShadowOffsetY": 0,
|
||||
"subtitleShadowBlur": 0,
|
||||
"subtitleShadowEnabled": false,
|
||||
"subtitleShadowLayers": [
|
||||
{
|
||||
"id": 1765435591571.2886,
|
||||
"offsetX": -2,
|
||||
"offsetY": -2,
|
||||
"blur": 2,
|
||||
"color": "#FFFFFF",
|
||||
"opacity": 60,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435591571.856,
|
||||
"offsetX": 2,
|
||||
"offsetY": 2,
|
||||
"blur": 0,
|
||||
"color": "#000000",
|
||||
"opacity": 65,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435591571.3809,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"blur": 4,
|
||||
"color": "#000000",
|
||||
"opacity": 50,
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"subtitlePosition": {
|
||||
"x": 51.3888888888889,
|
||||
"y": 23.333333333333304
|
||||
},
|
||||
"subtitleRotation": 0,
|
||||
"subtitleDirection": "horizontal",
|
||||
"subtitleCharSpacing": 12,
|
||||
"subtitleLineSpacing": 72,
|
||||
"subtitleMaxCharsPerLine": 15,
|
||||
"subtitleBackgroundEnabled": false,
|
||||
"subtitleBackgroundColor": "#000000",
|
||||
"subtitleBackgroundOpacity": 70,
|
||||
"subtitleBackgroundShape": "rectangle",
|
||||
"subtitleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"subtitleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 60
|
||||
},
|
||||
"subtitleBackgroundPoints": [],
|
||||
"subtitleBackgroundRadius": 10,
|
||||
"subtitleBackgroundRotation": 0,
|
||||
"maskEnabled": false,
|
||||
"maskImagePath": null,
|
||||
"maskSize": 100,
|
||||
"maskPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"maskColor": null,
|
||||
"maskOpacity": 100,
|
||||
"maskShape": "rectangle"
|
||||
}
|
||||
}
|
||||
}
|
||||
176
templates/cover-template-红黄虚描.json
Normal file
176
templates/cover-template-红黄虚描.json
Normal file
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"description": "封面模板导出配置",
|
||||
"timestamp": "2026-06-02T12:14:00.925Z",
|
||||
"coverTemplate": {
|
||||
"id": "sb8mm7l4bjf-mkdite6i",
|
||||
"name": "红黄虚描",
|
||||
"config": {
|
||||
"backgroundEnabled": false,
|
||||
"backgroundImagePath": null,
|
||||
"backgroundBlurEnabled": true,
|
||||
"backgroundBlurIntensity": 60,
|
||||
"backgroundSize": 100,
|
||||
"backgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"personImagePath": null,
|
||||
"personSize": 65.06944444444444,
|
||||
"personPosition": {
|
||||
"x": 62.73148148148148,
|
||||
"y": 67.0138888888889
|
||||
},
|
||||
"personRotation": 0,
|
||||
"personBorderEnabled": true,
|
||||
"personBorderStyle": "dashed",
|
||||
"personBorderColor": "#FFFFFF",
|
||||
"personBorderWidth": 12,
|
||||
"personBorderDashLength": null,
|
||||
"personBorderGapLength": null,
|
||||
"autoSplitText": true,
|
||||
"titleMaxLength": 4,
|
||||
"subtitleMaxLength": 10,
|
||||
"titleText": "托德揭秘",
|
||||
"titleFontFamily": "泼墨体",
|
||||
"titleFontSize": 165,
|
||||
"titleFontWeight": 100,
|
||||
"titleColor": "#d4ec74",
|
||||
"titleStrokeColor": "#000000",
|
||||
"titleStrokeWidth": 0,
|
||||
"titleShadowColor": "#000000",
|
||||
"titleShadowOffsetX": 0,
|
||||
"titleShadowOffsetY": 0,
|
||||
"titleShadowBlur": 0,
|
||||
"titleShadowEnabled": false,
|
||||
"titleShadowLayers": [
|
||||
{
|
||||
"id": 1765435502861.9124,
|
||||
"offsetX": -2,
|
||||
"offsetY": -2,
|
||||
"blur": 12,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435502861.0488,
|
||||
"offsetX": 2,
|
||||
"offsetY": 2,
|
||||
"blur": 16,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435502861.4207,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"blur": 14,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"titlePosition": {
|
||||
"x": 55.09259259259257,
|
||||
"y": 15.59027777777779
|
||||
},
|
||||
"titleRotation": 0,
|
||||
"titleDirection": "horizontal",
|
||||
"titleCharSpacing": 19,
|
||||
"titleLineSpacing": 118,
|
||||
"titleMaxCharsPerLine": 9,
|
||||
"titleBackgroundEnabled": false,
|
||||
"titleBackgroundColor": "#000000",
|
||||
"titleBackgroundOpacity": 70,
|
||||
"titleBackgroundShape": "rectangle",
|
||||
"titleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"titleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 30
|
||||
},
|
||||
"titleBackgroundPoints": [],
|
||||
"titleBackgroundRadius": 10,
|
||||
"titleBackgroundRotation": 0,
|
||||
"subtitleText": "芒格投资稳的原因",
|
||||
"subtitleFontFamily": "MaokenAssortedSans-Lite",
|
||||
"subtitleFontSize": 117,
|
||||
"subtitleFontWeight": 500,
|
||||
"subtitleColor": "#7e1010",
|
||||
"subtitleStrokeColor": "#ffffff",
|
||||
"subtitleStrokeWidth": 6,
|
||||
"subtitleShadowColor": "#000000",
|
||||
"subtitleShadowOffsetX": 0,
|
||||
"subtitleShadowOffsetY": 0,
|
||||
"subtitleShadowBlur": 0,
|
||||
"subtitleShadowEnabled": false,
|
||||
"subtitleShadowLayers": [
|
||||
{
|
||||
"id": 1765435591571.2886,
|
||||
"offsetX": -2,
|
||||
"offsetY": -2,
|
||||
"blur": 2,
|
||||
"color": "#FFFFFF",
|
||||
"opacity": 60,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435591571.856,
|
||||
"offsetX": 2,
|
||||
"offsetY": 2,
|
||||
"blur": 0,
|
||||
"color": "#000000",
|
||||
"opacity": 65,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435591571.3809,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"blur": 4,
|
||||
"color": "#000000",
|
||||
"opacity": 50,
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"subtitlePosition": {
|
||||
"x": 18.98148148148148,
|
||||
"y": 64.47916666666667
|
||||
},
|
||||
"subtitleRotation": 0,
|
||||
"subtitleDirection": "vertical",
|
||||
"subtitleCharSpacing": 12,
|
||||
"subtitleLineSpacing": 72,
|
||||
"subtitleMaxCharsPerLine": 15,
|
||||
"subtitleBackgroundEnabled": false,
|
||||
"subtitleBackgroundColor": "#000000",
|
||||
"subtitleBackgroundOpacity": 70,
|
||||
"subtitleBackgroundShape": "rectangle",
|
||||
"subtitleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"subtitleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 60
|
||||
},
|
||||
"subtitleBackgroundPoints": [],
|
||||
"subtitleBackgroundRadius": 10,
|
||||
"subtitleBackgroundRotation": 0,
|
||||
"maskEnabled": false,
|
||||
"maskImagePath": null,
|
||||
"maskSize": 100,
|
||||
"maskPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"maskColor": null,
|
||||
"maskOpacity": 100,
|
||||
"maskShape": "rectangle"
|
||||
}
|
||||
}
|
||||
}
|
||||
139
templates/cover-template-蓝底白字.json
Normal file
139
templates/cover-template-蓝底白字.json
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"description": "封面模板导出配置",
|
||||
"timestamp": "2026-06-02T12:13:43.982Z",
|
||||
"coverTemplate": {
|
||||
"id": "6404718a-1b1c-4148-9807-ad3c57a53e0c",
|
||||
"name": "蓝底白字",
|
||||
"config": {
|
||||
"backgroundEnabled": false,
|
||||
"backgroundImagePath": null,
|
||||
"backgroundBlurEnabled": false,
|
||||
"backgroundBlurIntensity": 10,
|
||||
"backgroundSize": 100,
|
||||
"backgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"personImagePath": null,
|
||||
"personSize": 80,
|
||||
"personPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"personRotation": 0,
|
||||
"personBorderEnabled": false,
|
||||
"personBorderStyle": "solid",
|
||||
"personBorderColor": "#FFFFFF",
|
||||
"personBorderWidth": 8,
|
||||
"personBorderDashLength": null,
|
||||
"personBorderGapLength": null,
|
||||
"autoSplitText": true,
|
||||
"titleMaxLength": 4,
|
||||
"subtitleMaxLength": 5,
|
||||
"titleText": "礼貌回归",
|
||||
"titleFontFamily": "USMCCyuanjiantecu",
|
||||
"titleFontSize": 120,
|
||||
"titleFontWeight": 700,
|
||||
"titleColor": "#cbcdb1",
|
||||
"titleStrokeColor": "#000000",
|
||||
"titleStrokeWidth": 0,
|
||||
"titleShadowColor": "#000000",
|
||||
"titleShadowOffsetX": 0,
|
||||
"titleShadowOffsetY": 0,
|
||||
"titleShadowBlur": 0,
|
||||
"titleShadowEnabled": false,
|
||||
"titleShadowLayers": [
|
||||
{
|
||||
"id": 1765449844664.8303,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"blur": 8,
|
||||
"color": "#000000",
|
||||
"opacity": 80,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765449844664.415,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"blur": 16,
|
||||
"color": "#000000",
|
||||
"opacity": 40,
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"titlePosition": {
|
||||
"x": 50.694444444444436,
|
||||
"y": 63.506944444444436
|
||||
},
|
||||
"titleRotation": 0,
|
||||
"titleDirection": "horizontal",
|
||||
"titleCharSpacing": 24,
|
||||
"titleLineSpacing": 144,
|
||||
"titleMaxCharsPerLine": 10,
|
||||
"titleBackgroundEnabled": false,
|
||||
"titleBackgroundColor": "#000000",
|
||||
"titleBackgroundOpacity": 70,
|
||||
"titleBackgroundShape": "rectangle",
|
||||
"titleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"titleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 30
|
||||
},
|
||||
"titleBackgroundPoints": [],
|
||||
"titleBackgroundRadius": 10,
|
||||
"titleBackgroundRotation": 0,
|
||||
"subtitleText": "行动亲爱的",
|
||||
"subtitleFontFamily": "千图厚黑体",
|
||||
"subtitleFontSize": 187,
|
||||
"subtitleFontWeight": 100,
|
||||
"subtitleColor": "#FFFFFF",
|
||||
"subtitleStrokeColor": "#000000",
|
||||
"subtitleStrokeWidth": 10,
|
||||
"subtitleShadowColor": "#000000",
|
||||
"subtitleShadowOffsetX": 0,
|
||||
"subtitleShadowOffsetY": 0,
|
||||
"subtitleShadowBlur": 0,
|
||||
"subtitleShadowEnabled": false,
|
||||
"subtitleShadowLayers": [],
|
||||
"subtitlePosition": {
|
||||
"x": 49.53703703703704,
|
||||
"y": 74.20138888888889
|
||||
},
|
||||
"subtitleRotation": 0,
|
||||
"subtitleDirection": "horizontal",
|
||||
"subtitleCharSpacing": -23,
|
||||
"subtitleLineSpacing": 72,
|
||||
"subtitleMaxCharsPerLine": 15,
|
||||
"subtitleBackgroundEnabled": true,
|
||||
"subtitleBackgroundColor": "#599dd9",
|
||||
"subtitleBackgroundOpacity": 25,
|
||||
"subtitleBackgroundShape": "rectangle",
|
||||
"subtitleBackgroundSize": {
|
||||
"width": 161.0185185185185,
|
||||
"height": 30.48611111111111
|
||||
},
|
||||
"subtitleBackgroundPosition": {
|
||||
"x": 50.23148148148148,
|
||||
"y": 67.11805555555556
|
||||
},
|
||||
"subtitleBackgroundPoints": [],
|
||||
"subtitleBackgroundRadius": 10,
|
||||
"subtitleBackgroundRotation": 0,
|
||||
"maskEnabled": false,
|
||||
"maskImagePath": null,
|
||||
"maskSize": 100,
|
||||
"maskPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"maskColor": null,
|
||||
"maskOpacity": 100,
|
||||
"maskShape": "rectangle"
|
||||
}
|
||||
}
|
||||
}
|
||||
167
templates/cover-template-青蓝虚描.json
Normal file
167
templates/cover-template-青蓝虚描.json
Normal file
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"description": "封面模板导出配置",
|
||||
"timestamp": "2026-06-02T12:13:52.918Z",
|
||||
"coverTemplate": {
|
||||
"id": "1xwrrcvf7a5-mkdite6i",
|
||||
"name": "青蓝虚描",
|
||||
"config": {
|
||||
"backgroundEnabled": false,
|
||||
"backgroundImagePath": null,
|
||||
"backgroundBlurEnabled": true,
|
||||
"backgroundBlurIntensity": 100,
|
||||
"backgroundSize": 100,
|
||||
"backgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"personImagePath": null,
|
||||
"personSize": 56.73611111111111,
|
||||
"personPosition": {
|
||||
"x": 35.87962962962963,
|
||||
"y": 71.52777777777777
|
||||
},
|
||||
"personRotation": 0,
|
||||
"personBorderEnabled": true,
|
||||
"personBorderStyle": "dashed",
|
||||
"personBorderColor": "#149fc2",
|
||||
"personBorderWidth": 14,
|
||||
"personBorderDashLength": null,
|
||||
"personBorderGapLength": null,
|
||||
"autoSplitText": true,
|
||||
"titleMaxLength": 4,
|
||||
"subtitleMaxLength": 11,
|
||||
"titleText": "托德揭秘",
|
||||
"titleFontFamily": "泼墨体",
|
||||
"titleFontSize": 115,
|
||||
"titleFontWeight": 350,
|
||||
"titleColor": "#149fc2",
|
||||
"titleStrokeColor": "#000000",
|
||||
"titleStrokeWidth": 7,
|
||||
"titleShadowColor": "#000000",
|
||||
"titleShadowOffsetX": 0,
|
||||
"titleShadowOffsetY": 0,
|
||||
"titleShadowBlur": 0,
|
||||
"titleShadowEnabled": false,
|
||||
"titleShadowLayers": [
|
||||
{
|
||||
"id": 1765435502861.9124,
|
||||
"offsetX": -2,
|
||||
"offsetY": -2,
|
||||
"blur": 12,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435502861.0488,
|
||||
"offsetX": 2,
|
||||
"offsetY": 2,
|
||||
"blur": 16,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435502861.4207,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"blur": 14,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"titlePosition": {
|
||||
"x": 90.9722222222222,
|
||||
"y": 34.16666666666667
|
||||
},
|
||||
"titleRotation": 0,
|
||||
"titleDirection": "vertical",
|
||||
"titleCharSpacing": 50,
|
||||
"titleLineSpacing": 118,
|
||||
"titleMaxCharsPerLine": 10,
|
||||
"titleBackgroundEnabled": false,
|
||||
"titleBackgroundColor": "#000000",
|
||||
"titleBackgroundOpacity": 70,
|
||||
"titleBackgroundShape": "rectangle",
|
||||
"titleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"titleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 30
|
||||
},
|
||||
"titleBackgroundPoints": [],
|
||||
"titleBackgroundRadius": 10,
|
||||
"titleBackgroundRotation": 0,
|
||||
"subtitleText": "芒格投资稳的原因",
|
||||
"subtitleFontFamily": "杨任东竹石体",
|
||||
"subtitleFontSize": 87,
|
||||
"subtitleFontWeight": 500,
|
||||
"subtitleColor": "#149fc2",
|
||||
"subtitleStrokeColor": "#000000",
|
||||
"subtitleStrokeWidth": 0,
|
||||
"subtitleShadowColor": "#000000",
|
||||
"subtitleShadowOffsetX": 0,
|
||||
"subtitleShadowOffsetY": 0,
|
||||
"subtitleShadowBlur": 0,
|
||||
"subtitleShadowEnabled": false,
|
||||
"subtitleShadowLayers": [
|
||||
{
|
||||
"id": 1768540523070.3923,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"blur": 2,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1768540523070.0796,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"blur": 2,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"subtitlePosition": {
|
||||
"x": 76.85185185185186,
|
||||
"y": 58.40277777777778
|
||||
},
|
||||
"subtitleRotation": 0,
|
||||
"subtitleDirection": "vertical",
|
||||
"subtitleCharSpacing": 12,
|
||||
"subtitleLineSpacing": 72,
|
||||
"subtitleMaxCharsPerLine": 15,
|
||||
"subtitleBackgroundEnabled": false,
|
||||
"subtitleBackgroundColor": "#000000",
|
||||
"subtitleBackgroundOpacity": 70,
|
||||
"subtitleBackgroundShape": "rectangle",
|
||||
"subtitleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"subtitleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 60
|
||||
},
|
||||
"subtitleBackgroundPoints": [],
|
||||
"subtitleBackgroundRadius": 10,
|
||||
"subtitleBackgroundRotation": 0,
|
||||
"maskEnabled": false,
|
||||
"maskImagePath": null,
|
||||
"maskSize": 100,
|
||||
"maskPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"maskColor": null,
|
||||
"maskOpacity": 100,
|
||||
"maskShape": "rectangle"
|
||||
}
|
||||
}
|
||||
}
|
||||
176
templates/cover-template-黄白实描.json
Normal file
176
templates/cover-template-黄白实描.json
Normal file
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"description": "封面模板导出配置",
|
||||
"timestamp": "2026-06-02T12:13:46.948Z",
|
||||
"coverTemplate": {
|
||||
"id": "485183be-6eed-4eae-b74a-8591f08b69fa",
|
||||
"name": "黄白实描",
|
||||
"config": {
|
||||
"backgroundEnabled": false,
|
||||
"backgroundImagePath": null,
|
||||
"backgroundBlurEnabled": true,
|
||||
"backgroundBlurIntensity": 30,
|
||||
"backgroundSize": 100,
|
||||
"backgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"personImagePath": null,
|
||||
"personSize": 70.27777777777777,
|
||||
"personPosition": {
|
||||
"x": 35.87962962962963,
|
||||
"y": 64.75694444444444
|
||||
},
|
||||
"personRotation": 0,
|
||||
"personBorderEnabled": true,
|
||||
"personBorderStyle": "solid",
|
||||
"personBorderColor": "#FFFFFF",
|
||||
"personBorderWidth": 32,
|
||||
"personBorderDashLength": null,
|
||||
"personBorderGapLength": null,
|
||||
"autoSplitText": true,
|
||||
"titleMaxLength": 4,
|
||||
"subtitleMaxLength": 6,
|
||||
"titleText": "置顶96",
|
||||
"titleFontFamily": "1.程荣光刻楷",
|
||||
"titleFontSize": 300,
|
||||
"titleFontWeight": 1000,
|
||||
"titleColor": "#ffde05",
|
||||
"titleStrokeColor": "#000000",
|
||||
"titleStrokeWidth": 0,
|
||||
"titleShadowColor": "#000000",
|
||||
"titleShadowOffsetX": 0,
|
||||
"titleShadowOffsetY": 0,
|
||||
"titleShadowBlur": 0,
|
||||
"titleShadowEnabled": false,
|
||||
"titleShadowLayers": [
|
||||
{
|
||||
"id": 1765435502861.9124,
|
||||
"offsetX": -2,
|
||||
"offsetY": -2,
|
||||
"blur": 12,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435502861.0488,
|
||||
"offsetX": 2,
|
||||
"offsetY": 2,
|
||||
"blur": 16,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435502861.4207,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"blur": 14,
|
||||
"color": "#000000",
|
||||
"opacity": 100,
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"titlePosition": {
|
||||
"x": 91.89814814814812,
|
||||
"y": 66.45833333333334
|
||||
},
|
||||
"titleRotation": 0,
|
||||
"titleDirection": "vertical",
|
||||
"titleCharSpacing": -20,
|
||||
"titleLineSpacing": 118,
|
||||
"titleMaxCharsPerLine": 10,
|
||||
"titleBackgroundEnabled": false,
|
||||
"titleBackgroundColor": "#000000",
|
||||
"titleBackgroundOpacity": 70,
|
||||
"titleBackgroundShape": "rectangle",
|
||||
"titleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"titleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 30
|
||||
},
|
||||
"titleBackgroundPoints": [],
|
||||
"titleBackgroundRadius": 10,
|
||||
"titleBackgroundRotation": 0,
|
||||
"subtitleText": "4高丹草新品",
|
||||
"subtitleFontFamily": "USMCCyuanjiantecu",
|
||||
"subtitleFontSize": 117,
|
||||
"subtitleFontWeight": 500,
|
||||
"subtitleColor": "#FFFFFF",
|
||||
"subtitleStrokeColor": "#000000",
|
||||
"subtitleStrokeWidth": 0,
|
||||
"subtitleShadowColor": "#000000",
|
||||
"subtitleShadowOffsetX": 0,
|
||||
"subtitleShadowOffsetY": 0,
|
||||
"subtitleShadowBlur": 0,
|
||||
"subtitleShadowEnabled": false,
|
||||
"subtitleShadowLayers": [
|
||||
{
|
||||
"id": 1765435591571.2886,
|
||||
"offsetX": -2,
|
||||
"offsetY": -2,
|
||||
"blur": 2,
|
||||
"color": "#FFFFFF",
|
||||
"opacity": 60,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435591571.856,
|
||||
"offsetX": 2,
|
||||
"offsetY": 2,
|
||||
"blur": 0,
|
||||
"color": "#000000",
|
||||
"opacity": 65,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 1765435591571.3809,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"blur": 4,
|
||||
"color": "#000000",
|
||||
"opacity": 50,
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"subtitlePosition": {
|
||||
"x": 47.45370370370371,
|
||||
"y": 12.743055555555557
|
||||
},
|
||||
"subtitleRotation": 0,
|
||||
"subtitleDirection": "horizontal",
|
||||
"subtitleCharSpacing": 12,
|
||||
"subtitleLineSpacing": 72,
|
||||
"subtitleMaxCharsPerLine": 15,
|
||||
"subtitleBackgroundEnabled": false,
|
||||
"subtitleBackgroundColor": "#000000",
|
||||
"subtitleBackgroundOpacity": 70,
|
||||
"subtitleBackgroundShape": "rectangle",
|
||||
"subtitleBackgroundSize": {
|
||||
"width": 30,
|
||||
"height": 10
|
||||
},
|
||||
"subtitleBackgroundPosition": {
|
||||
"x": 50,
|
||||
"y": 60
|
||||
},
|
||||
"subtitleBackgroundPoints": [],
|
||||
"subtitleBackgroundRadius": 10,
|
||||
"subtitleBackgroundRotation": 0,
|
||||
"maskEnabled": false,
|
||||
"maskImagePath": null,
|
||||
"maskSize": 100,
|
||||
"maskPosition": {
|
||||
"x": 50,
|
||||
"y": 50
|
||||
},
|
||||
"maskColor": null,
|
||||
"maskOpacity": 100,
|
||||
"maskShape": "rectangle"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user