聆听历史的回响——多源异构数据采集与融合应用综合实践
这个项目属于哪个课程 | 2024数据采集与融合技术实践 |
---|---|
组名、项目简介 | 组名:scrapy能帮我爬到美味蟹黄堡的秘方吗 项目需求:文物不能很好的融入我们的生活,它们仿佛一具冰冷的尸体躺在博物馆的展示柜中,静静地接受着岁月的侵蚀和尘埃的覆盖。 项目目标:赋予文物新的生命力,让它们“动”起来。通过人工智能技术,为文物创造更加精彩动人的故事,生成曼妙的声音,制作生动震撼的展示视频,让历史以全新的方式触动人心。 技术路线:vue3前端web网站搭建,python flask后端,华为云平台(服务器接口调用),阿里云平台(部分数据存储),kimi背景故事AI生成,百度ai文本转语音,快手可灵ai平台对口型视频生成和通过接口实现视频生成 |
团队成员学号 | 曹星才-072208130 张诗悦-052205144 朱佳杰-012202239 黄悦佳-102202142 詹镇壕-102202149 |
这个项目目标 | 将静止的文物与现代科技相结合,赋予它们新的生命和表现力。我们通过先进的人工智能技术,让文物“动”起来,呈现出它们沉睡千年的故事与情感。AI将为每件文物创作更加丰富和感人的背景故事,让它们不再只是历史的见证者,而是活生生的叙述者。同时,通过AI生成曼妙的声音,让这些故事更加生动,引人入胜;借助视觉技术,生成精美的展示视频,生动再现文物的诞生过程、文化背景与历史场景。最终,我们希望通过这一系列创新方式,让更多人触及文物的灵魂,感受历史与文化的魅力。 |
其他参考文献 | https://element-plus.org/zh-CN/component/overview.html https://cn.vuejs.org/guide/introduction w3school 在线教程 uiverse.io Apache ECharts https://docs.qingque.cn/d/home/eZQClW07IFEuX1csc-VejdY2M?identityId=1oEG9JKKMFv#section=h.a6acy8mosh |
码云链接(代码已汇总,各小组成员代码不分开放) | 前端:综合设计实践——前端 ·2022级数据采集与融合技术 - 码云 - 开源中国 后端:综合设计实践——后端 ·2022级数据采集与融合技术 - 码云 - 开源中国 爬虫:https://gitee.com/jia1666372886/museumspider/tree/master/ |
一、项目背景
文物不能很好的融入我们的生活,它们仿佛一具冰冷的尸体躺在博物馆的展示柜中,静静地接受着岁月的侵蚀和尘埃的覆盖。那些曾经承载着历史脉络与文化荣光的物品,如今只剩下了沉默的外壳,无法向我们诉说它们曾经的故事。它们被时间的长河淹没,渐渐失去了与我们生活的联系,变成了一个个静止的符号,等待着被无数的目光定格,却难以真正融入现代人的日常。
这些文物或许曾在千百年前激荡起社会的波澜,或许曾是某个伟大文明的象征,但如今它们的存在仿佛和现代世界隔绝了。我们虽然能够从它们的外观上看到历史的痕迹,感受到当时的技艺与智慧,却难以触摸到那段历史背后的人情冷暖、风云变幻。它们不再是生活的一部分,而是被孤立在某个遥远的过去,只是某种文化的遗物。
然而,尽管它们远离了现代的喧嚣,文物依然是人类历史与文化的见证者。每一件文物都有自己独特的生命力,它们承载着前人的思想、情感和智慧。要想让文物重新焕发出活力,我们需要跨越时空的藩篱,用现代科技去唤醒它们沉睡的记忆。通过人工智能、虚拟现实等技术,文物可以不再是冰冷的展品,而是成为我们与过去沟通的桥梁。它们能够重新讲述那个时代的故事,带领我们走进千年之前的世界,让我们重新感受到历史的脉搏和文化的力量。
在这个数字化、智能化的时代,我们不妨通过创新的方式,去打破文物与现代生活之间的隔阂,让这些承载着历史与文明的珍宝重新走进我们的生活,让它们不再只是博物馆里的静物,而是活跃在我们日常的对话和思考之中。
二、项目分工
成员 | 任务 |
---|---|
曹星才 | 前端页面设计,接口调度与路由跳转逻辑,js代码编写 |
张诗悦 | 前端页面设计,web原型设计,css样式与html框架设计 |
朱佳杰 | 文物信息爬取 |
黄悦佳 | 后端服务器搭建与数据库管理 |
詹镇壕 | 后端服务器搭建与api接口编写 |
三、个人贡献
路由、接口以及相关js代码介绍
(一)路由跳转逻辑
3.1.1 导航栏
导航栏跳转:主页、展示页、列表(管理)页、管理员中心(登陆)页
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: NavigationBar,
children: [
{ path: '/home', component: Home },
{ path: '/exhibit', component: Exhibit },
{ path: '/management', component: Management },
{ path: '/personal', component: Personal },
{ path: '', redirect: '/home' },
],
},
],
})
3.1.2 根据文物id跳转到相应的详细页
{
path: '/artifact/:id', // 路径包含文物的 ID
name: 'ArtifactDetail',
component: Detail,
props: (route) => ({
id: route.params.id,
}),
},
3.1.3 根据文物id跳转到相应的文物修改页
{
path: '/artifactEdit/:id', // 路径包含文物的 ID
name: 'ArtifactEdit',
component: Add,
props: (route) => ({
id: route.params.id,
name: route.query.name,
period: route.query.period,
category: route.query.category,
parameter: route.query.parameter,
material: route.query.material,
thumbnail_path: route.query.thumbnail_path,
description: route.query.description,
}),
},
3.1.4 添加页与分析页
{ path: '/add', name:'add',component: Add },
{ path: '/analyse', name:'analyse',component: Analyse },
(二)接口介绍
接口地址设计:
const axiosInstance = axios.create({ baseURL: 'http://***.***.***.***:****', //后端地址 timeout: 1000000, });
3.2.1 获取文物
全部获取:
export const get_artifacts = () => {
return axiosInstance.get(`/api/artifacts`)
}
部分获取:
export const get_artifacts2 = async (limit = 20) => {
try {
const response = await axiosInstance.get(`/api/artifacts`, {
params: {
limit,
}
});
return response.data; // 返回文物数据
} catch (error) {
console.error("Error fetching artifacts:", error);
throw error;
}
};
根据id获取:
export const get_artifacts_by_id = (id) => {
return axiosInstance.get(`/api/artifacts/${id}`)
}
根据名字获取:
export const get_artifact_by_name = async (name, limit = 1) => {
try {
const response = await axiosInstance.get(`/api/artifacts/search`, {
params: {
name: name,
limit,
}
});
return response.data; // 返回文物数据
} catch (error) {
console.error("Error fetching artifacts:", error);
throw error;
}
};
调用:
const loading = ref(true)
// 获取文物列表的函数
const fetchArtifacts = async () => {
try {
const response = await get_artifacts() // 调用获取文物的 API 函数
console.log(response)
artifactStore.setArtifactData(response.data.data) // 获取全部
artifacts.value = artifactStore.$state.artifact // 更新本地的数据
} catch (error) {
console.error('获取文物列表失败:', error)
alert('获取文物列表失败')
} finally {
loading.value = false // 结束加载状态
}
}
3.2.2 添加文物
export const add_artifact = (artifactData) => {
// 获取当前存储的 token(例如从 Pinia store 或 localStorage)
const accessToken = localStorage.getItem('access_token'); // 或者使用 Pinia store 获取 token
if (!accessToken) {
// 如果没有 token,提示用户登录
return Promise.reject(new Error('请先登录'));
}
// 发送 POST 请求并携带 Authorization header
return axiosInstance.post('/api/artifacts', artifactData, {
headers: {
Authorization: `Bearer ${accessToken}`, // 将 token 加入请求头
},
}).then(response => {
if (response.status === 201) {
console.log('添加文物信息成功');
return response.data; // 返回成功的响应数据
} else {
throw new Error(`添加文物信息失败,状态码:${response.status}`);
}
}).catch(error => {
console.error('添加文物信息请求失败:', error);
throw error;
});
};
3.2.3 更新文物
export const update_artifact = (id, artifactData) => {
// 获取当前存储的 token(例如从 localStorage 或 Pinia)
const accessToken = localStorage.getItem('access_token'); // 或者使用 Pinia store 获取 token
if (!accessToken) {
// 如果没有 token,提示用户登录
return Promise.reject(new Error('请先登录'));
}
// 构建请求头
const headers = {
Authorization: `Bearer ${accessToken}`, // 将 token 加入请求头
};
// 发送 PUT 请求以更新文物信息
return axiosInstance.put(`/api/artifacts/${id}`, artifactData, {
headers: headers,
})
.then((response) => {
if (response.status === 200) {
console.log('文物信息更新成功');
return response.data; // 返回响应数据
} else {
console.error(`更新文物失败,状态码:${response.status}`);
throw new Error(`更新文物失败,状态码:${response.status}`);
}
})
.catch((error) => {
console.error('更新文物信息失败:', error);
throw error;
});
};
调用:
const submitForm = async (formEl) => {
if (!formEl) return
await formEl.validate(async (valid, fields) => {
if (valid) {
if (isEdit.value) {
// 更新文物
await update_artifact(ruleForm.id, ruleForm)
fetchArtifacts()
} else {
// 添加文物
await add_artifact(ruleForm)
fetchArtifacts()
}
// 跳转或显示成功信息
router.push('/management')
} else {
console.log('error submit!', fields)
}
})
}
3.2.4 删除文物
export const test_delete_artifact = (id) => {
// 获取当前存储的 token(例如从 Pinia store)
const accessToken = localStorage.getItem('access_token'); // 或者使用 Pinia store 获取 token
if (!accessToken) {
// 如果没有 token,提示用户登录
return Promise.reject(new Error('请先登录'));
}
// 发送 DELETE 请求并携带 Authorization header
return axiosInstance.delete(`/api/artifacts/${id}`, {
headers: {
Authorization: `Bearer ${accessToken}`, // 将 token 加入请求头
},
});
};
调用:
const handleDelete = async (id) => {
try {
await test_delete_artifact(id) // 调用删除函数
alert('文物删除成功')
// 删除成功后更新界面
} catch (error) {
alert(error.message) // 显示错误消息(如没有登录)
}
}
3.2.5 登陆
export const login = (account, password) => {
return axiosInstance.post('/api/users/login', {
phone: account,
password: password
})
}
调用:
const handleLogin = async () => {
try {
const response = await login(account.value, password.value)
if (response.status === 200) {
// 获取并存储 JWT 令牌
const accessToken = response.data.access_token
authStore.setAccessToken(accessToken)
ElMessage({
message: '登录成功',
type: 'success',
customClass: 'el-message-success',
})
}
} catch (error) {
console.error('登录失败', error)
ElMessage({
message: '登录失败,请检查账号和密码,如果没有管理员账号请联系平台开发者',
type: 'error',
})
}
}
3.2.6 添加用户交互记录
export const add_user_interaction = (artifactId, interactionType='view') => {
// 获取当前存储的 token(例如从 localStorage 或 Pinia store)
const accessToken = localStorage.getItem('access_token'); // 或者使用 Pinia store 获取 token
if (!accessToken) {
// 如果没有 token,提示用户登录
return Promise.reject(new Error('请先登录'));
}
// 请求数据
const interactionData = {
artifact_id: artifactId,
interaction_type: interactionType, // 交互类型,如 'view'
};
// 发送 POST 请求并携带 Authorization header
return axiosInstance.post('/api/interactions', interactionData, {
headers: {
Authorization: `Bearer ${accessToken}`, // 将 token 加入请求头
},
});
};
调用:
const handleAction = async (id_) => {
try {
// 先记录用户的交互行为
await add_user_interaction(id_, 'view') // 'view' 表示查看交互类型
console.log(`交互记录已添加,文物 ID: ${id_}`)
} catch (error) {
console.error('添加交互记录失败:', error.message)
} finally {
// 然后跳转到文物详情页面
router.push({
name: 'ArtifactDetail',
params: { id: id_ },
})
}
}
3.2.7 获取交互记录
根据类别获取:
export const get_interactions_count_by_category = async () => {
try {
const response = await axiosInstance.get('/api/interactions/category/interactions_count');
return response.data; // 返回文物类别交互数量统计的数据
} catch (error) {
console.error("Error fetching interactions count by category:", error);
throw error; // 抛出错误,供上层处理
}
};
调用:
const fetchAndRenderChart = async () => {
try {
// 获取文物类别统计数据
const countData = await get_artifacts_count_by_category()
// 提取数据部分
const categoryData = countData.data
console.log(categoryData)
// 格式化数据,转换为图表需要的形式
const chartData = categoryData.map((item) => ({
value: item.artifact_count,
name: item.category,
}))
// 图表配置
const option1 = {
title: {
text: '各类别数量占比',
left: 'center',
textStyle: {
fontSize: 30,
},
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)',
},
legend: {
top: 'bottom',
},
toolbox: {
show: true,
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
restore: { show: true },
saveAsImage: { show: true },
},
},
series: [
{
name: 'Percentage!',
type: 'pie',
radius: [50, 250],
center: ['50%', '50%'],
roseType: 'area',
itemStyle: {
borderRadius: 8,
},
data: chartData, // 动态填充数据
},
],
}
// 获取图表 DOM 容器
const myChart1 = echarts.init(chart1.value)
// 渲染图表
myChart1.setOption(option1)
} catch (error) {
console.error('获取文物类别统计数量失败', error)
}
}
获取每个文物的交互记录:
export const get_artifact_interaction_counts = async (limit = 10) => {
try {
const response = await axiosInstance.get('/api/interactions/count', {
params: {
limit, // 设置请求的 limit 参数
},
});
return response.data; // 返回数据
} catch (error) {
console.error("Error fetching artifact interaction counts:", error);
throw error;
}
};
调用:
const fetchAndRenderInteractionChart = async () => {
try {
// 获取文物交互次数数据
const interactionData = await get_interactions_count_by_category()
console.log('dasdas', interactionData.data)
// 格式化数据
const artifactIds = interactionData.data.map((item) => item.category) // 文物ID(或者名称)
const interactionCounts = interactionData.data.map((item) => item.category_interaction_count) // 交互次数
// 图表配置
const option2 = {
title: {
text: '各类别文物热度',
left: 'center',
textStyle: {
fontSize: 30,
},
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
toolbox: {
show: true,
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
restore: { show: true },
saveAsImage: { show: true },
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: artifactIds, // 更新为从 API 获取的文物 ID
axisTick: {
alignWithLabel: true,
},
},
],
yAxis: [
{
type: 'value',
},
],
series: [
{
name: 'Count',
type: 'bar',
barWidth: '60%',
data: interactionCounts, // 更新为从 API 获取的交互次数
},
],
}
// 获取图表 DOM 容器
const myChart2 = echarts.init(chart2.value)
// 渲染图表
myChart2.setOption(option2)
} catch (error) {
console.error('获取文物统计数量失败', error)
}
}
3.2.8 AI接口
AI故事:
export const get_artifact_story_by_id = (id) => {
return axiosInstance.get(`/api/ai_story/${id}`)
}
AI语音:
export const generateAudio = async (text) => {
try {
const response = await axiosInstance.post('/api/tts', {
text: text, // 传递需要转换为语音的文本
});
return response.data.audio_url; // 返回音频的URL
} catch (error) {
console.error("Error generating audio:", error);
throw error; // 抛出错误,便于外部捕获
}
};
AI视频:
export const generateVideo = async (imageUrl) => {
try {
// 构建查询参数
const params = {
url: imageUrl
};
// 发送 GET 请求
const response = await axiosInstance.get('/api/ai_video', { params });
// 检查响应状态
if (response.status === 200) {
console.log('生成视频测试通过');
console.log(response.data); // 输出返回的 JSON 数据
return response.data; // 返回视频生成的相关信息
} else {
console.error(`生成视频信息失败,状态码:${response.status}, 响应内容:${response.data}`);
throw new Error(`生成视频信息失败,状态码:${response.status}`);
}
} catch (error) {
console.error('请求错误:', error);
throw error; // 抛出错误,便于外部捕获
}
};
调用:
const loading = ref(false)
const handleAI = async (id) => {
try {
ai_button.value=false
loading.value = true
const response =await get_artifact_story_by_id(id)
console.log(response)
story.value=response.data.data
compiledMarkdown.value = marked( story.value)
if (compiledMarkdown.value) {
generateAudioAndPlay(); // 使用 `compiledMarkdown.value`
}
generateVideoPlay()
} catch (error) {
alert(error.message)
}finally{
loading.value = false
v.value=true
}
}
// 用于存储音频的 URL
const audioUrl = ref(null)
// 用于显示加载状态
const loading2 = ref(false)
// 用 ref 创建对音频元素的引用
const audioPlayer = ref(null)
// 生成音频并播放
const generateAudioAndPlay = async () => {
// 先清除之前的音频 URL
audioUrl.value = null
loading2.value = true
try {
// 调用 API 生成音频并获取音频 URL
const url = await generateAudio(extractTextFromHtml(compiledMarkdown.value))
// 设置音频 URL
audioUrl.value = url
// 使用 ref 引用播放音频
if (audioPlayer.value) {
audioPlayer.value.play()
}
} catch (error) {
console.error('音频生成失败:', error)
alert('音频生成失败,请重试')
} finally {
loading2.value = false
}
}
const videoUrl=ref(null)
const loading3=ref(false)
// 生成视频并获取 URL
const generateVideoPlay = async () => {
// 先清除之前的音频 URL
videoUrl.value = null
loading3.value = true
try {
console.log(artifact.value.thumbnail_path)
const videoData = await generateVideo(artifact.value.thumbnail_path); // 示例图片 URL
console.log("22222",videoData)
videoUrl.value = videoData.data; // 假设返回的数据中有 `videoUrl` 字段
} catch (error) {
console.error('视频生成失败:', error);
}finally{
loading3.value=false
}
};
const extractTextFromHtml = (html) => {
console.log(html)
const doc = new DOMParser().parseFromString(html, 'text/html');
return doc.body.textContent || ''; // 返回纯文本
};
(三)js代码相关
3.3.1 文物筛选
// 筛选的类别
const selectedCategory = ref('')
// const search=ref(false)
// 过滤后的文物列表(根据筛选条件)
const filteredArtifacts = computed(() => {
return selectedCategory.value
? artifacts.value.filter((item) => item.category === selectedCategory.value)
: artifacts.value
})
3.3.2 分析图表初始化
onMounted(() => {
// 调用函数来获取数据并渲染图表
fetchAndRenderChart()
fetchAndRenderInteractionChart()
initChart3()
})
3.3.3 页面详细页接收跳转参数
onMounted(() => {
const { id } = route.params
if (id) {
isEdit.value = true
formTitle.value = '更新文物'
const { name, period, category, parameter, material, thumbnail_path, description } = route.query
ruleForm.id = id || ''
ruleForm.name = name || ''
ruleForm.period = period || ''
ruleForm.category = category || ''
ruleForm.parameter = parameter || ''
ruleForm.material = material || ''
ruleForm.description = description || ''
ruleForm.thumbnail_path = thumbnail_path || ''
}
})
3.3.4 首页轮播图随机播放
const randomArtifacts = computed(() => {
const numToSelect = 10 // 选择6个文物
return getRandomItems(artifacts.value, numToSelect)
})
function getRandomItems(array, num) {
const result = []
const seenIndexes = new Set()
while (result.length < num && result.length < array.length) {
const randomIndex = Math.floor(Math.random() * array.length)
// 确保索引不重复
if (!seenIndexes.has(randomIndex)) {
result.push(array[randomIndex])
seenIndexes.add(randomIndex)
}
}
return result
}
3.3.5 展示页子组件向父组件传参
import Checkbox from '@/components/Checkbox.vue'
// 传世品、革命文物、国史文物、货币、考古发掘品、名族名俗文物、古籍文献、外国文物、艺术品
const sorts = ref([
{
text: '传世品',
isChecked: true,
},
{
text: '货币',
isChecked: true,
},
{
text: '艺术品',
isChecked: true,
},
{
text: '革命文物',
isChecked: true,
},
{
text: '国史文物',
isChecked: true,
},
{
text: '考古发掘品',
isChecked: true,
},
{
text: '民族民俗文物',
isChecked: true,
},
{
text: '古籍文献',
isChecked: true,
},
{
text: '外国文物',
isChecked: true,
},
]);
// 父组件传递的更新数据函数
const emit = defineEmits(['categoryChanged'])
// 监听复选框变化
const handleCheckboxChange = () => {
// 过滤选中的类别
const selectedCategories = sorts.value
.filter(item => item.isChecked)
.map(item => item.text)
.filter(Boolean); // 过滤掉任何假值,如 undefined 或 null
// 发送选中的类别给父组件
emit('categoryChanged',selectedCategories);
};
onMounted(() => {
handleCheckboxChange()
});
3.3.6 监听管理员转换管理员模式
watch(isAdmin, (newVal) => {
if (newVal) {
router.push({
name: 'ArtifactEdit',
params: { id: artifact.value.id },
query: {
name: artifact.value.name,
period: artifact.value.period,
category: artifact.value.category,
parameter: artifact.value.parameter,
material: artifact.value.material,
thumbnail_path: artifact.value.thumbnail_path,
description: artifact.value.description,
},
})
}
})
3.3.7 按钮点击事件及AI事件触发
const handleAI = async (id) => {
try {
ai_button.value=false
loading.value = true
const response =await get_artifact_story_by_id(id)
console.log(response)
story.value=response.data.data
compiledMarkdown.value = marked( story.value)
if (compiledMarkdown.value) {
generateAudioAndPlay(); // 使用 `compiledMarkdown.value`
}
generateVideoPlay()
} catch (error) {
alert(error.message)
}finally{
loading.value = false
v.value=true
}
}
(四)全局变量stores存储
3.4.1 全部文物
export const useArtifactStore = defineStore('artifact', {
state: () => ({
artifact: JSON.parse(localStorage.getItem('artifactData')) || [], // 从 localStorage 获取数据,如果没有就初始化为空数组
}),
actions: {
setArtifactData(artifact) {
console.log("store", artifact);
this.artifact = artifact;
// 将数据保存到 localStorage
localStorage.setItem('artifactData', JSON.stringify(this.artifact));
console.log("store2", this.artifact);
},
clearArtifactData() {
this.artifact = [];
localStorage.removeItem('artifactData'); // 清除缓存
},
},
});
3.4.2 管理员模式全局状态管理
export const useAdminStore = defineStore('admin', {
state: () => ({
isAdmin: false, // 管理员模式的状态
}),
actions: {
toggleAdminMode() {
// console.log('Before toggle:', this.isAdmin);
this.isAdmin = !this.isAdmin; // 切换管理员模式状态
// console.log('aaaaa:', this.isAdmin);
}
}
});
export const useAuthStore = defineStore('auth', {
state: () => ({
accessToken: localStorage.getItem('access_token') || null, // 初始化时从 localStorage 获取令牌(如果存在)
}),
actions: {
setAccessToken(token) {
this.accessToken = token
localStorage.setItem('access_token', token) // 存储到 localStorage
},
clearAccessToken() {
this.accessToken = null
localStorage.removeItem('access_token') // 从 localStorage 删除
},
},
getters: {
isAuthenticated: (state) => !!state.accessToken, // 判断是否登录
}
})
四、收获
对我来说,这次实践真是一次宝贵的成长经历。我负责的是前端部分,使用了Vue 3的组合式API,并通过单文件组件进行开发。刚开始时,面临了很多挑战,例如页面跳转不成功、参数无法正确传递到下一页、JS代码编写的难题以及接口请求不正常或返回的参数不对等问题。每一个问题都曾让我感到有些迷茫,但在不断的摸索和调试中,我最终找到了合适的解决方案。
通过这次实践,我不仅学到了更多关于Vue 3的使用技巧,还提升了问题解决能力。学会了如何在Vue中灵活地管理状态和处理异步请求,如何优化页面跳转流程以及如何确保接口的数据传输准确无误;学会了如何与后端顺利的对接;学会了如何协调组员,互帮互助,做好每一步工作。
前端部分是繁琐的,每调整一步都要全局修改探索,在不断摸爬滚打中,我战胜了自己,让自己掌握了更多的专业知识。