修改路径为URL,不下载,临时上库2

This commit is contained in:
Hommy
2026-04-23 16:58:48 +08:00
parent 7f8a9fc0a7
commit d77a27a8b0
8 changed files with 86 additions and 134 deletions
+23 -1
View File
@@ -1,4 +1,4 @@
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_validator
from typing import Optional
@@ -13,6 +13,28 @@ class EasyCreateMaterialRequest(BaseModel):
font_size: int = Field(default=15, description="字体大小")
text_transform_y: int = Field(default=0, description="文字Y轴位置偏移")
@staticmethod
def _is_http_url(url: str) -> bool:
return isinstance(url, str) and url.startswith(("http://", "https://"))
@field_validator("audio_url")
@classmethod
def validate_audio_url(cls, value: str) -> str:
"""音频 URL 必须为 http/https。"""
if not cls._is_http_url(value):
raise ValueError("audio_url must start with http:// or https://")
return value
@field_validator("img_url", "video_url")
@classmethod
def validate_optional_media_url(cls, value: Optional[str]) -> Optional[str]:
"""可选图片/视频 URL 如有传值,必须为 http/https。"""
if value is None:
return value
if not cls._is_http_url(value):
raise ValueError("optional media URL must start with http:// or https://")
return value
class EasyCreateMaterialResponse(BaseModel):
"""快速创建素材轨道响应参数"""
+4 -19
View File
@@ -3,9 +3,7 @@ from src.pyJianYingDraft import ScriptFile, trange, AudioSceneEffectType, VideoS
import src.pyJianYingDraft as draft
from src.utils.draft_cache import DRAFT_CACHE
from exceptions import CustomException, CustomError
import os
from src.utils import helper
import config
import json
import asyncio
from typing import List, Dict, Any, Tuple, Optional
@@ -43,14 +41,12 @@ def _add_audios_internal(
draft_id = validate_and_get_draft_id(draft_url)
script: ScriptFile = DRAFT_CACHE[draft_id]
draft_audio_dir = create_audio_directory(draft_id)
audios = parse_audio_data(json_str=audio_infos)
validate_audio_data(audios, draft_id)
track_name = add_audio_track(script)
audio_ids = add_audio_segments(script, track_name, draft_audio_dir, audios)
audio_ids = add_audio_segments(script, track_name, audios)
script.save()
logger.info(f"Draft saved successfully")
@@ -128,15 +124,6 @@ def validate_and_get_draft_id(draft_url: str) -> str:
return draft_id
def create_audio_directory(draft_id: str) -> str:
"""创建音频资源存储目录"""
draft_dir = os.path.join(config.DRAFT_DIR, draft_id)
draft_audio_dir = os.path.join(draft_dir, "assets", "audios")
os.makedirs(name=draft_audio_dir, exist_ok=True)
logger.info(f"Created audio directory: {draft_audio_dir}")
return draft_audio_dir
def validate_audio_data(audios: List[Dict[str, Any]], draft_id: str):
"""验证音频数据是否为空"""
if len(audios) == 0:
@@ -154,12 +141,12 @@ def add_audio_track(script: ScriptFile) -> str:
return track_name
def add_audio_segments(script: ScriptFile, track_name: str, draft_audio_dir: str, audios: List[Dict[str, Any]]) -> List[str]:
def add_audio_segments(script: ScriptFile, track_name: str, audios: List[Dict[str, Any]]) -> List[str]:
"""批量添加音频片段到指定轨道"""
audio_ids = []
for i, audio in enumerate(audios):
try:
audio_id = add_audio_to_draft(script, track_name, draft_audio_dir=draft_audio_dir, audio=audio)
audio_id = add_audio_to_draft(script, track_name, audio=audio)
audio_ids.append(audio_id)
logger.info(f"Added audio {i+1}/{len(audios)}, audio_id: {audio_id}")
except Exception as e:
@@ -264,7 +251,6 @@ def add_audio_effect(audio_segment, audio_effect: str):
def add_audio_to_draft(
script: ScriptFile,
track_name: str,
draft_audio_dir: str,
audio: dict
) -> str:
"""
@@ -273,7 +259,6 @@ def add_audio_to_draft(
Args:
script: 草稿文件对象
track_name: 音频轨道名称
draft_audio_dir: 音频资源目录
audio: 音频信息字典,包含以下字段:
audio_url: 音频URL
duration: 音频总时长(微秒),可选字段
@@ -320,7 +305,7 @@ def add_audio_to_draft(
return audio_segment.material_instance.material_id
except CustomException:
logger.error(f"Add audio to draft failed, draft_audio_dir: {draft_audio_dir}, audio: {audio}")
logger.error(f"Add audio to draft failed, audio: {audio}")
raise
except Exception as e:
logger.error(f"Add audio to draft failed, error: {str(e)}")
+1 -11
View File
@@ -4,9 +4,7 @@ import src.pyJianYingDraft as draft
from src.utils.draft_cache import DRAFT_CACHE
from exceptions import CustomException, CustomError
from src.schemas.add_images import SegmentInfo
import os
from src.utils import helper
import config
import json
import asyncio
from typing import List, Dict, Any, Tuple, Optional
@@ -93,11 +91,6 @@ def _add_images_internal(
logger.error(f"Invalid draft URL or draft not found in cache, draft_id: {draft_id}")
raise CustomException(CustomError.INVALID_DRAFT_URL)
draft_dir = os.path.join(config.DRAFT_DIR, draft_id)
draft_image_dir = os.path.join(draft_dir, "assets", "images")
os.makedirs(name=draft_image_dir, exist_ok=True)
logger.info(f"Using image directory: {draft_image_dir}")
images = parse_image_data(json_str=image_infos)
if len(images) == 0:
logger.error(f"No image info provided, draft_id: {draft_id}")
@@ -117,7 +110,6 @@ def _add_images_internal(
try:
segment_id, segment_info = add_image_to_draft(
script, track_name,
draft_image_dir=draft_image_dir,
image=image,
alpha=alpha,
scale_x=scale_x,
@@ -224,7 +216,6 @@ async def add_images_async(
def add_image_to_draft(
script: ScriptFile,
track_name: str,
draft_image_dir: str,
image: dict,
alpha: float = 1.0,
scale_x: float = 1.0,
@@ -238,7 +229,6 @@ def add_image_to_draft(
Args:
script: 草稿文件对象
track_name: 视频轨道名称
draft_image_dir: 图片资源目录
image: 图片信息字典,包含以下字段:
image_url: 图片URL
width: 图片宽度(像素)
@@ -395,7 +385,7 @@ def add_image_to_draft(
return video_segment.segment_id, segment_info
except CustomException:
logger.error(f"Add image to draft failed, draft_image_dir: {draft_image_dir}, image: {image}")
logger.error(f"Add image to draft failed, image: {image}")
raise
except Exception as e:
logger.error(f"Add image to draft failed, error: {str(e)}")
+2 -11
View File
@@ -6,9 +6,7 @@ from src.pyJianYingDraft import ScriptFile, trange, IntroType
import src.pyJianYingDraft as draft
from src.utils.draft_cache import DRAFT_CACHE
from exceptions import CustomException, CustomError
import os
from src.utils import helper
import config
import json
from typing import List, Dict, Any, Tuple, Optional
from src.utils.draft_lock_manager import get_draft_lock_manager
@@ -193,11 +191,6 @@ def _add_videos_internal(
if (not draft_id) or (draft_id not in DRAFT_CACHE):
raise CustomException(CustomError.INVALID_DRAFT_URL)
# 2. 创建保存视频资源的目录
draft_dir = os.path.join(config.DRAFT_DIR, draft_id)
draft_video_dir = os.path.join(draft_dir, "assets", "videos")
os.makedirs(name=draft_video_dir, exist_ok=True)
videos = parse_video_data(json_str=video_infos)
if len(videos) == 0:
logger.info(f"No video info, draft_id: {draft_id}")
@@ -237,7 +230,7 @@ def _add_videos_internal(
video['end'] = video['start'] + original_duration
logger.info(f"Adjusted video {i} start time to {video['start']} for continuity, original_duration: {original_duration}")
segment_id, actual_duration = add_video_to_draft(script, track_name, draft_video_dir=draft_video_dir, video=video,
segment_id, actual_duration = add_video_to_draft(script, track_name, video=video,
scene_timeline=scene_timeline,
alpha=alpha, scale_x=scale_x, scale_y=scale_y,
transform_x=transform_x, transform_y=transform_y)
@@ -295,7 +288,6 @@ def _has_valid_scene_timelines(
def add_video_to_draft(
script: ScriptFile,
track_name: str,
draft_video_dir: str,
video: dict,
scene_timeline: Optional[Dict[str, int]] = None,
alpha: float = 1.0,
@@ -310,7 +302,6 @@ def add_video_to_draft(
Args:
script: 草稿文件对象
track_name: 视频轨道名称
draft_video_dir: 视频资源目录
video: 视频信息字典,包含以下字段:
video_url: 视频URL
width: 视频宽度(像素)
@@ -414,7 +405,7 @@ def add_video_to_draft(
# 8. 返回片段ID和实际播放时长(注意:是segment_id而不是material_id
return video_segment.segment_id, actual_duration
except CustomException:
logger.info(f"Add video to draft failed, draft_video_dir: {draft_video_dir}, video: {video}")
logger.info(f"Add video to draft failed, video: {video}")
raise
except Exception as e:
logger.error(f"Add video to draft failed, error: {str(e)}")
+35 -83
View File
@@ -1,7 +1,5 @@
import json
import os
from typing import Optional
from urllib.parse import urlparse
import asyncio
from src.utils.logger import logger
@@ -10,7 +8,6 @@ import src.pyJianYingDraft as draft
from src.utils.draft_cache import DRAFT_CACHE
from exceptions import CustomException, CustomError
from src.utils import helper
import config
from src.utils.draft_lock_manager import DraftLockManager
@@ -52,36 +49,31 @@ def easy_create_material(
logger.error(f"Invalid draft_url or draft not found in cache: {draft_url}")
raise CustomException(CustomError.INVALID_DRAFT_URL)
# 2. 验证音频URL(必选参数)
if not audio_url or audio_url.lower() == "null" or audio_url.strip() == "":
logger.error("Audio URL is required and cannot be empty or null")
raise CustomException(CustomError.MATERIAL_CREATE_FAILED)
# 3. 从缓存中获取草稿
# 2. 从缓存中获取草稿
script: ScriptFile = DRAFT_CACHE[draft_id]
logger.info(f"Retrieved script from cache, draft_id: {draft_id}")
try:
# 4. 添加音频(必须的)
audio_added = add_audio_material(script, draft_id, audio_url)
# 3. 添加音频(必须的)
audio_added = add_audio_material(script, audio_url)
logger.info(f"Audio material added: {audio_added}")
# 5. 添加视频(如果提供)
if video_url and video_url.strip() and video_url.lower() != "null":
video_added = add_video_material(script, draft_id, video_url)
# 4. 添加视频(如果提供)
if video_url:
video_added = add_video_material(script, video_url)
logger.info(f"Video material added: {video_added}")
# 6. 添加图片(如果提供)
if img_url and img_url.strip() and img_url.lower() != "null":
image_added = add_image_material(script, draft_id, img_url)
# 5. 添加图片(如果提供)
if img_url:
image_added = add_image_material(script, img_url)
logger.info(f"Image material added: {image_added}")
# 7. 添加文字(如果提供)
# 6. 添加文字(如果提供)
if text and text.strip():
text_added = add_text_material(script, text, text_color, font_size, text_transform_y)
logger.info(f"Text material added: {text_added}")
# 8. 保存草稿
# 7. 保存草稿
script.save()
logger.info(f"Draft saved successfully")
@@ -174,18 +166,16 @@ async def easy_create_material_async(
logger.info(f"Lock released for draft_id: {draft_id}")
def add_video_material(script: ScriptFile, draft_id: str, video_url: str) -> bool:
def add_video_material(script: ScriptFile, video_url: str) -> bool:
"""
添加视频素材到草稿(固定5秒时长)
与add_videos接口保持一致的处理方式:
1. 创建规范的视频资源目录
2. 使用parse_video_data解析视频信息
3. 调用add_video_to_draft添加视频
1. 使用parse_video_data解析视频信息
2. 调用add_video_to_draft添加视频
Args:
script: 草稿文件对象
draft_id: 草稿ID
video_url: 视频文件URL
Returns:
@@ -194,13 +184,7 @@ def add_video_material(script: ScriptFile, draft_id: str, video_url: str) -> boo
try:
logger.info(f"Adding video material: {video_url}")
# 1. 创建视频资源目录(与add_videos保持一致
draft_dir = os.path.join(config.DRAFT_DIR, draft_id)
draft_video_dir = os.path.join(draft_dir, "assets", "videos")
os.makedirs(name=draft_video_dir, exist_ok=True)
logger.info(f"Created video directory: {draft_video_dir}")
# 2. 构造视频信息JSON(固定5秒时长)
# 1. 构造视频信息JSON(固定5秒时长
video_infos = json.dumps([{
"video_url": video_url,
"width": 1920,
@@ -211,20 +195,20 @@ def add_video_material(script: ScriptFile, draft_id: str, video_url: str) -> boo
"volume": 1.0
}])
# 3. 解析视频信息
# 2. 解析视频信息
from src.service.add_videos import parse_video_data, add_video_to_draft
video_items = parse_video_data(video_infos)
if not video_items:
logger.error("No video items parsed")
return False
# 4. 添加视频轨道
# 3. 添加视频轨道
track_name = f"video_track_{helper.gen_unique_id()}"
script.add_track(track_type=TrackType.video, track_name=track_name, relative_index=10)
logger.info(f"Added video track: {track_name}")
# 5. 添加视频到轨道(传递正确的视频资源目录)
add_video_to_draft(script, track_name, draft_video_dir, video_items[0])
# 4. 添加视频到轨道
add_video_to_draft(script, track_name, video_items[0])
return True
except Exception as e:
@@ -232,18 +216,16 @@ def add_video_material(script: ScriptFile, draft_id: str, video_url: str) -> boo
return False
def add_image_material(script: ScriptFile, draft_id: str, img_url: str) -> bool:
def add_image_material(script: ScriptFile, img_url: str) -> bool:
"""
添加图片素材到草稿
与add_images接口保持一致的处理方式:
1. 创建规范的图片资源目录
2. 使用parse_image_data解析图片信息
3. 调用add_image_to_draft添加图片
1. 使用parse_image_data解析图片信息
2. 调用add_image_to_draft添加图片
Args:
script: 草稿文件对象
draft_id: 草稿ID
img_url: 图片文件URL
Returns:
@@ -252,13 +234,7 @@ def add_image_material(script: ScriptFile, draft_id: str, img_url: str) -> bool:
try:
logger.info(f"Adding image material: {img_url}")
# 1. 创建图片资源目录(与add_images保持一致
draft_dir = os.path.join(config.DRAFT_DIR, draft_id)
draft_image_dir = os.path.join(draft_dir, "assets", "images")
os.makedirs(name=draft_image_dir, exist_ok=True)
logger.info(f"Created image directory: {draft_image_dir}")
# 2. 构造图片信息JSON(默认尺寸和3秒显示时长)
# 1. 构造图片信息JSON(默认尺寸和3秒显示时长
image_infos = json.dumps([{
"image_url": img_url,
"width": 1024,
@@ -267,20 +243,20 @@ def add_image_material(script: ScriptFile, draft_id: str, img_url: str) -> bool:
"end": 3000000, # 3秒(3 * 1000000微秒)
}])
# 3. 解析图片信息
# 2. 解析图片信息
from src.service.add_images import parse_image_data, add_image_to_draft
image_items = parse_image_data(image_infos)
if not image_items:
logger.error("No image items parsed")
return False
# 4. 添加图片轨道
# 3. 添加图片轨道
track_name = f"image_track_{helper.gen_unique_id()}"
script.add_track(track_type=TrackType.video, track_name=track_name, relative_index=10)
logger.info(f"Added image track: {track_name}")
# 5. 添加图片到轨道(传递正确的图片资源目录)
add_image_to_draft(script, track_name, draft_image_dir, image_items[0])
# 4. 添加图片到轨道
add_image_to_draft(script, track_name, image_items[0])
return True
except Exception as e:
@@ -288,18 +264,16 @@ def add_image_material(script: ScriptFile, draft_id: str, img_url: str) -> bool:
return False
def add_audio_material(script: ScriptFile, draft_id: str, audio_url: str) -> bool:
def add_audio_material(script: ScriptFile, audio_url: str) -> bool:
"""
添加音频素材到草稿(固定5秒时长)
与add_audios接口保持一致的处理方式:
1. 创建规范的音频资源目录
2. 使用parse_audio_data解析音频信息
3. 调用add_audio_to_draft添加音频
1. 使用parse_audio_data解析音频信息
2. 调用add_audio_to_draft添加音频
Args:
script: 草稿文件对象
draft_id: 草稿ID
audio_url: 音频文件URL
Returns:
@@ -308,13 +282,7 @@ def add_audio_material(script: ScriptFile, draft_id: str, audio_url: str) -> boo
try:
logger.info(f"Adding audio material: {audio_url}")
# 1. 创建音频资源目录(与add_audios保持一致
draft_dir = os.path.join(config.DRAFT_DIR, draft_id)
draft_audio_dir = os.path.join(draft_dir, "assets", "audios")
os.makedirs(name=draft_audio_dir, exist_ok=True)
logger.info(f"Created audio directory: {draft_audio_dir}")
# 2. 构造音频信息JSON(固定5秒时长)
# 1. 构造音频信息JSON(固定5秒时长
audio_infos = json.dumps([{
"audio_url": audio_url,
"start": 0,
@@ -322,20 +290,20 @@ def add_audio_material(script: ScriptFile, draft_id: str, audio_url: str) -> boo
"volume": 1.0
}])
# 3. 解析音频信息
# 2. 解析音频信息
from src.service.add_audios import parse_audio_data, add_audio_to_draft
audio_items = parse_audio_data(audio_infos)
if not audio_items:
logger.error("No audio items parsed")
return False
# 4. 添加音频轨道
# 3. 添加音频轨道
track_name = f"audio_track_{helper.gen_unique_id()}"
script.add_track(track_type=TrackType.audio, track_name=track_name, relative_index=10)
logger.info(f"Added audio track: {track_name}")
# 5. 添加音频到轨道(传递正确的音频资源目录)
add_audio_to_draft(script, track_name, draft_audio_dir, audio_items[0])
# 4. 添加音频到轨道
add_audio_to_draft(script, track_name, audio_items[0])
return True
except Exception as e:
@@ -435,19 +403,3 @@ def hex_to_rgb(hex_color: str) -> list:
logger.warning(f"Failed to parse hex color: {hex_color}, using default white")
return [1.0, 1.0, 1.0] # 默认白色
def validate_url(url: str) -> bool:
"""
验证URL格式是否正确
Args:
url: 待验证的URL
Returns:
URL是否有效
"""
try:
result = urlparse(url)
return all([result.scheme, result.netloc])
except Exception:
return False
-3
View File
@@ -48,7 +48,6 @@ class TestAddVideosAsync:
# Mock 所有依赖
with patch('src.service.add_videos.helper.get_url_param') as mock_get_param, \
patch('src.service.add_videos.DRAFT_CACHE') as mock_cache, \
patch('src.service.add_videos.os.makedirs'), \
patch('src.service.add_videos.parse_video_data') as mock_parse, \
patch('src.service.add_videos.add_video_to_draft') as mock_add:
@@ -148,7 +147,6 @@ class TestAddVideosAsync:
with patch("src.service.add_videos.helper.get_url_param", return_value="concurrent-test"), \
patch("src.service.add_videos.DRAFT_CACHE") as mock_cache, \
patch("src.service.add_videos.os.makedirs"), \
patch("src.service.add_videos.parse_video_data", return_value=[{
"video_url": "https://example.com/video.mp4",
"start": 0,
@@ -221,7 +219,6 @@ class TestAddVideosAsync:
"duration": 5000000,
}]), \
patch("src.service.add_videos.DRAFT_CACHE") as mock_cache, \
patch("src.service.add_videos.os.makedirs"), \
patch("src.service.add_videos.add_video_to_draft") as mock_add:
mock_cache.__contains__.return_value = True
+1 -3
View File
@@ -134,9 +134,7 @@ class TestAllAsyncLockAPIs:
with patch('src.service.add_images.DRAFT_CACHE') as mock_cache, \
patch('src.service.add_images.helper.get_url_param', return_value=mock_draft_data['draft_id']), \
patch('src.service.add_images.draft.VideoSegment', return_value=mock_img_seg), \
patch('src.service.add_images.os.makedirs'), \
patch('src.service.add_images.os.path.isfile', return_value=True):
patch('src.service.add_images.draft.VideoSegment', return_value=mock_img_seg):
# 模拟草稿对象(prepare 阶段会执行 draft_id in DRAFT_CACHE
mock_script = MagicMock()
+20 -3
View File
@@ -10,6 +10,7 @@ from src.pyJianYingDraft.local_materials import VideoMaterial, AudioMaterial
from src.schemas.add_images import AddImagesRequest
from src.schemas.add_videos import AddVideosRequest
from src.schemas.add_audios import AddAudiosRequest
from src.schemas.easy_create_material import EasyCreateMaterialRequest
@pytest.fixture
@@ -33,7 +34,6 @@ def test_add_image_to_draft_uses_url_for_material_path(draft_ctx):
add_image_to_draft(
script=draft_ctx["script"],
track_name="image_track_1",
draft_image_dir="/tmp/ignored",
image={
"image_url": "https://assets.jcaigc.cn/demo1.png",
"width": 1024,
@@ -56,7 +56,6 @@ def test_add_video_to_draft_uses_url_for_material_path(draft_ctx):
add_video_to_draft(
script=draft_ctx["script"],
track_name="video_track_1",
draft_video_dir="/tmp/ignored",
video={
"video_url": "https://assets.jcaigc.cn/demo1.mp4",
"start": 0,
@@ -77,7 +76,6 @@ def test_add_audio_to_draft_uses_url_for_material_path(draft_ctx):
add_audio_to_draft(
script=draft_ctx["script"],
track_name="audio_track_1",
draft_audio_dir="/tmp/ignored",
audio={
"audio_url": "https://assets.jcaigc.cn/demo1.mp3",
"start": 0,
@@ -151,3 +149,22 @@ def test_schema_rejects_non_http_audio_url():
"end": 1000,
}]),
)
def test_easy_create_material_schema_rejects_non_http_audio_url():
"""easy_create_material 的 audio_url 应在 schema 阶段校验。"""
with pytest.raises(Exception):
EasyCreateMaterialRequest(
draft_url="http://localhost/v1/get_draft?draft_id=draft-1",
audio_url="file:///tmp/demo1.mp3",
)
def test_easy_create_material_schema_rejects_non_http_optional_media_url():
"""easy_create_material 的 img_url/video_url 如有值必须为 http/https。"""
with pytest.raises(Exception):
EasyCreateMaterialRequest(
draft_url="http://localhost/v1/get_draft?draft_id=draft-1",
audio_url="https://assets.jcaigc.cn/test1.mp3",
img_url="ftp://example.com/demo1.png",
)