diff --git a/meeting_summarizer/README.md b/meeting_summarizer/README.md new file mode 100644 index 000000000..f6a74158f --- /dev/null +++ b/meeting_summarizer/README.md @@ -0,0 +1,119 @@ +# Meeting Summarizer for Odoo 18 + +[![Odoo](https://img.shields.io/badge/Odoo-%23A24689.svg?style=for-the-badge&logo=Odoo&logoColor=white)](https://www.odoo.com) +[![License](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](https://opensource.org/licenses/MIT) + +## Overview + +The Meeting Summarizer module transcribes Discuss meetings and saves the +transcript along with a summary. + +## Features + +- Features +- πŸ“„ Download the transcription summary file. +- πŸ“ Access and download the full transcription data. +- βœ‰οΈ Automatically send transcription and summary files +to selected users. + + +## Screenshots + +Here are some glimpses of Json Widget: + +### User Interface + +
+ + + Feature 1 + + +
+
+ + + Feature 1 + + +
+
+ + + Feature 1 + + +
+
+ + + Feature 1 + + +
+ + +## Prerequisites + +Before you begin, ensure you have the following installed: + +- An active Odoo Community/Enterprise Edition instance (local or hosted) + +## Configuration +- Ensure the OpenAI Python package is installed. + +## Installation + +Follow these steps to set up and run the app: + +1. **Clone the Repository** + + ```git clone https://github.com/cybrosystech/Meeting-Summarize.git``` + +2. **Add the module to addons** + + ```cd Meeting-Summarize``` + +## Contributing + +We welcome contributions! Currently, this feature is supported only in Google Chrome. +You’re welcome to contribute and help extend compatibility to other browsers. + +To get started: + +1. Fork the repository. + +2. Create a new branch: + ``` + git checkout -b feature/your-feature-name + ``` +3. Make changes and commit: + ``` + git commit -m "Add your message here" + ``` +4. Push your changes: + ``` + git push origin feature/your-feature-name + ``` +5. Create a Pull Request on GitHub. + +--- +- Submit a pull request with a clear description of your changes. + +## License + +This project is licensed under the AGPL-3. Feel free to use, modify, and distribute it as needed. + + +## Contact + +* Mail Contact : odoo@cybrosys.com +* Website : https://cybrosys.com + +Maintainer +========== +![Cybrosys Logo](https://www.cybrosys.com/images/logo.png) +https://cybrosys.com + +This module is maintained by Cybrosys Technologies. +For support and more information, please visit https://www.cybrosys.com \ No newline at end of file diff --git a/meeting_summarizer/__init__.py b/meeting_summarizer/__init__.py new file mode 100644 index 000000000..9569e695a --- /dev/null +++ b/meeting_summarizer/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU AFFERO +# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. +# +# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE +# (AGPL v3) along with this program. +# If not, see . +# +############################################################################# +from .import controller +from .import models diff --git a/meeting_summarizer/__manifest__.py b/meeting_summarizer/__manifest__.py new file mode 100644 index 000000000..12d7cec86 --- /dev/null +++ b/meeting_summarizer/__manifest__.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU AFFERO +# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. +# +# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE +# (AGPL v3) along with this program. +# If not, see . +# +############################################################################# +{ + 'name': 'Meeting Summarizer', + 'version': '18.0.1.0.0', + 'category': 'Extra Tools', + 'summary': """Transcribes Discuss meetings and + saves the text with a summary.""", + 'description': """This module transcribes the Discuss meeting and + saves the transcription in a file. It also generates a summary of the meeting content.""", + 'author': 'Cybrosys Techno Solutions', + 'company': 'Cybrosys Techno Solutions', + 'maintainer': 'Cybrosys Techno Solutions', + 'website': 'https://www.cybrosys.com', + 'depends':['mail', 'web'], + 'data':[ + 'security/ir.model.access.csv', + 'data/send_transcription_template.xml', + 'views/res_config_settings.xml', + 'views/send_mail_transcription.xml', + ], + "assets": { + 'web.assets_backend': [ + "meeting_summarizer/static/src/js/call_action_list.js", + "meeting_summarizer/static/src/js/attachment_list.js", + "meeting_summarizer/static/src/xml/attachment_list.xml", + ], + 'mail.assets_public': [ + "meeting_summarizer/static/src/js/call_action_list.js", + "meeting_summarizer/static/src/js/attachment_list.js", + "meeting_summarizer/static/src/xml/attachment_list.xml", + ], + }, + 'external_dependencies': { + 'python': ['openai'], + }, + 'images': ['static/description/banner.jpg'], + 'license': 'AGPL-3', + 'installable': True, + 'auto_install': False, + 'application': True, +} diff --git a/meeting_summarizer/controller/__init__.py b/meeting_summarizer/controller/__init__.py new file mode 100644 index 000000000..909ad1379 --- /dev/null +++ b/meeting_summarizer/controller/__init__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU AFFERO +# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. +# +# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE +# (AGPL v3) along with this program. +# If not, see . +# +############################################################################# +""" +This module handles the transcription-related functionalities for the application. + +It includes features for transcribing meeting recordings, processing audio data, +and generating transcription files. Additionally, the module may interface with +external APIs or services to improve transcription accuracy. + +The 'transcription' module is imported to manage specific transcription tasks. +""" +from . import transcription diff --git a/meeting_summarizer/controller/transcription.py b/meeting_summarizer/controller/transcription.py new file mode 100644 index 000000000..b6d1403be --- /dev/null +++ b/meeting_summarizer/controller/transcription.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU AFFERO +# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. +# +# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE +# (AGPL v3) along with this program. +# If not, see . +# +############################################################################# +""" +This module defines HTTP controllers for handling transcription-related requests +in the Meeting Summarizer module, including interactions with the OpenAI API. +""" +import base64 +import json +import openai + +from odoo import _ +from odoo import http +from odoo.exceptions import ValidationError +from odoo.http import request + + +class TranscriptionController(http.Controller): + """Transcription controllers""" + @http.route('/get/transcription_data', type='json', auth='public') + def get_transcription_file(self, **kwargs): + """Controller is used to store the transcription data""" + transcription_id = kwargs['id'] + if not transcription_id: + return {'error': 'No ID provided'} + cache_key = f"transcription_id_{transcription_id}" + stored_data = request.env['ir.config_parameter'].sudo().get_param( + cache_key) + if stored_data: + transcription_list = json.loads(stored_data) + else: + transcription_list = [] + transcription_list.append(kwargs) + request.env['ir.config_parameter'].sudo().set_param(cache_key, + json.dumps( + transcription_list)) + return {'message': 'Data stored successfully', 'cache_key': cache_key} + + @http.route('/create/transcription_file_summary', type='json', auth='user') + def get_cached_transcription_file(self, **kwargs): + """Get the data from ir_config parameter and create + transcription file and summary file in ir_attachment""" + transcription_id = kwargs.get('kwargs', {}).get('id') + if not transcription_id: + return {'error': 'No ID provided'} + cache_key = f"transcription_id_{transcription_id}" + cached_data = request.env['ir.config_parameter'].sudo().get_param( + cache_key) + if not cached_data: + return {'error': 'No cached data found'} + cached_data = json.loads(cached_data) + text_content = "\n".join(item["data"] for item in cached_data) + api_key = request.env['ir.config_parameter'].sudo().get_param( + "meeting_summarizer.open_api_key") + if not api_key: + raise ValidationError( + _("Please Enter a valid api key in settings..")) + client = openai.OpenAI(api_key=api_key) + def create_summary(content): + response = client.chat.completions.create( + model="gpt-4-turbo", + messages=[ + {"role": "system", + "content": "Summarize the following meeting transcript."}, + {"role": "user", "content": content} + ], + temperature=0.7, + max_tokens=500 + ) + return response + summary_data = create_summary(text_content) + summary_text = summary_data.choices[0].message.content # Corrected response extraction + file_data = base64.b64encode(summary_text.encode("utf-8")) + file_name = f"transcription_id_{transcription_id}.txt" + summary_file_name = f"summary_id_{transcription_id}.txt" + new_file_content = text_content.encode('utf-8') + new_file_base64 = base64.b64encode(new_file_content).decode('utf-8') + def get_attachment(attachment_name): + attachment_detail = request.env['ir.attachment'].sudo().search([ + ('name', '=', attachment_name), + ('res_model', '=', 'ir.attachment'), + ('res_id', '=', transcription_id) + ], limit=1) + return attachment_detail + attachment = get_attachment(file_name) + summary_attachment = get_attachment(summary_file_name) + + if attachment or summary_attachment: + existing_summary_content = ( + base64.b64decode(summary_attachment.datas).decode('utf-8')) + updated_summary_content = ( + existing_summary_content + "\n" + text_content) # Append new data + + attachment.sudo().write({ + 'datas': base64.b64encode( + text_content.encode('utf-8')).decode('utf-8') + }) + summary_data_update = ( + create_summary(updated_summary_content)) + summary_text = ( + summary_data_update.choices[0].message.content) # Corrected response extraction + summary_attachment.sudo().write({ + 'datas': base64.b64encode(summary_text.encode('utf-8')).decode( + 'utf-8') + }) + else: + def create_attachment(filename, datas): + file = request.env['ir.attachment'].sudo().create({ + 'name': filename, + 'datas': datas, + 'res_model': 'ir.attachment', + 'res_id': transcription_id, + 'type': 'binary', + 'mimetype': 'text/plain', + }) + return file + attachment = create_attachment(file_name, new_file_base64) + summary_attachment = create_attachment(summary_file_name, file_data) + return {'success': True, 'attachment_id': attachment.id, 'summary': summary_attachment.id} + + @http.route('/get/transcription_data/summary', type='json', auth='public') + def get_transcription_data_summary(self, **kwargs): + """Controller for return the specific transcription and summary file""" + transcription_id = False + summary_id = False + channel_id = kwargs['kwargs'].get('channelId', False) + attachments = request.env['ir.attachment'].sudo().search( + [('res_id', '=', int(channel_id)), + ('res_model', '=', 'ir.attachment')]) + + for attachment in attachments: + if attachment.name == f"transcription_id_{channel_id}.txt": + transcription_id = attachment.id + else: + summary_id = attachment.id + return {'transcriptionId': transcription_id, + 'summaryId': summary_id} + + @http.route('/create/send_transcription/record', type='json', auth='public') + def get_send_transcription_id(self, **kwargs): + """create a record in send_mail_transcription using partner_ids,subject + email_body,transcription_attachment_ids, summary_attachment_ids and + return the corresponding record id""" + subject = kwargs['kwargs'].get('subject') + email_body = kwargs['kwargs'].get('email_body') + partner_ids = kwargs['kwargs'].get('partnerIds') + transcription_id = kwargs['kwargs'].get('transcriptionId') + summary_id = kwargs['kwargs'].get('summaryId') + transcription_id = request.env['ir.attachment'].browse(transcription_id) + summary_id = request.env['ir.attachment'].browse(summary_id) + send_mail_transcription_id = request.env['send.mail.transcription'].create( + {'partner_ids': partner_ids, + 'subject': subject, + 'email_body': email_body, + 'transcription_attachment_ids': transcription_id, + 'summary_attachment_ids': summary_id}) + return send_mail_transcription_id.id + + @http.route('/check/auto_mail_send', type='json', auth='public') + def check_auto_mail_send(self, **kwargs): + """Here checking the configuration settings that auto_mail_send_option + enable or not and also get all the values from that and return the + specific participant details""" + channel_id = kwargs['kwargs'].get('channelId', False) + auto_mail_send = request.env['ir.config_parameter'].sudo().get_param( + "meeting_summarizer.auto_mail_send") + + select_users = request.env['ir.config_parameter'].sudo().get_param( + "meeting_summarizer.select_user") + + participants = [] + if auto_mail_send and select_users: + partner_details = request.env['discuss.channel.member'].sudo().search( + [('channel_id', '=', channel_id)] + ) + host = request.env['discuss.channel'].browse(channel_id) + for rec in partner_details: + user = request.env['res.users'].search( + [('partner_id', '=', rec.partner_id.id)] + ) + if user: + if select_users == 'host': + participants.append({ + 'partner_id': host.create_uid.partner_id.id, + 'email': host.create_uid.email + }) + break # Exit the loop after adding the host, no need for further logic + if user.has_group('base.group_user'): + participants.append({ + 'partner_id': rec.partner_id.id, + 'email': rec.partner_id.email + }) + return participants + + @http.route('/send/auto_email', type='json', auth='public') + def send_auto_mail(self, **kwargs): + """Here sending automatic mail for selected users in settings.""" + transcription_id = kwargs['kwargs'].get('transcriptionId') + summary_id = kwargs['kwargs'].get('summaryId') + email_body = kwargs['kwargs'].get('email_body') + subject = kwargs['kwargs'].get('subject') + partners_email = kwargs['kwargs'].get('partners_email', + []) + if not partners_email: + return { + 'error': 'No valid email addresses found for the selected partners.'} + from_mail = request.env.user.email + attachment_ids = [] + if transcription_id and summary_id: + attachment_ids.append((4, transcription_id)) + attachment_ids.append((4, summary_id)) + email_values = { + 'email_from': from_mail, + 'email_to': ','.join(partners_email), # Convert email list to string + 'subject': subject, + 'body_html': email_body, + 'attachment_ids': attachment_ids, # Attach files if available + } + email = request.env['mail.mail'].sudo().create(email_values) + email.send() + return True + + @http.route('/get/Meeting/creator', type='json', auth='public') + def get_meeting_creator(self, **kwargs): + """Return the channel creator""" + channel_id = kwargs['kwargs'].get('channelId', False) + channel_details = request.env['discuss.channel'].sudo().browse(channel_id) + return channel_details.create_uid.id + + @http.route('/attach/transcription_data/summary', type='json', + auth='public') + def attach_transcription_data_summary(self, **kwargs): + """Controller for return the specific transcription and summary file""" + transcription_id = False + summary_id = False + channel_id = kwargs['kwargs'].get('channelId', False) + attachments = request.env['ir.attachment'].sudo().search( + [('res_id', '=', int(channel_id)), + ('res_model', '=', 'ir.attachment')]) + for attachment in attachments: + if attachment.name == f"transcription_id_{channel_id}.txt": + transcription_id = attachment.id + else: + summary_id = attachment.id + + channel = request.env['discuss.channel'].sudo().browse(int(channel_id)) + attachment_ids = list(filter(None, [transcription_id, summary_id])) + odoo_bot_user = request.env.ref('base.user_root') + channel.with_user(odoo_bot_user).message_post( + body="πŸ“ Meeting transcription and summary are now available.", + message_type='comment', + subtype_xmlid='mail.mt_comment', + attachment_ids=attachment_ids + ) + return True diff --git a/meeting_summarizer/data/send_transcription_template.xml b/meeting_summarizer/data/send_transcription_template.xml new file mode 100644 index 000000000..d702f8f5a --- /dev/null +++ b/meeting_summarizer/data/send_transcription_template.xml @@ -0,0 +1,22 @@ + + + + + Account Report email template + + +
+
+

+ Hello, +
+ +

+
+ Regards, +
+ +
+
+
+
\ No newline at end of file diff --git a/meeting_summarizer/doc/RELEASE_NOTES.md b/meeting_summarizer/doc/RELEASE_NOTES.md new file mode 100644 index 000000000..bfa7340aa --- /dev/null +++ b/meeting_summarizer/doc/RELEASE_NOTES.md @@ -0,0 +1,8 @@ +## Module + +#### 25.07.2025 +#### Version 18.0.1.0.0 +#### ADD + +- Initial commit for Meeting Summarizer +- \ No newline at end of file diff --git a/meeting_summarizer/models/__init__.py b/meeting_summarizer/models/__init__.py new file mode 100644 index 000000000..4e4c39b98 --- /dev/null +++ b/meeting_summarizer/models/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU AFFERO +# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. +# +# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE +# (AGPL v3) along with this program. +# If not, see . +# +############################################################################# +from .import res_config_settings +from .import send_mail_transcription diff --git a/meeting_summarizer/models/res_config_settings.py b/meeting_summarizer/models/res_config_settings.py new file mode 100644 index 000000000..d01483a16 --- /dev/null +++ b/meeting_summarizer/models/res_config_settings.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU AFFERO +# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. +# +# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE +# (AGPL v3) along with this program. +# If not, see . +# +############################################################################# +""" +This module extends Odoo's ResConfigSettings to provide configuration options +for the Meeting Summarizer module, including Open API settings and email options. +""" +from odoo import models, fields + + +class ResConfigSettings(models.TransientModel): + """ + This class extends the res.config.settings model to add configuration options + for the Meeting Summarizer module. It allows users to configure: + + - Whether the Open API feature is enabled. + - The API key to use with the Open API. + - Whether to automatically send transcription emails. + - Who should receive the emails (host or all logged-in users). + + All fields are stored as system-wide configuration parameters. + """ + _inherit = "res.config.settings" + + open_api_value = fields.Boolean(string="Open API Key", + config_parameter="meeting_summarizer.open_api_value" ) + open_api_key = fields.Char(string="Open API Value", + config_parameter="meeting_summarizer.open_api_key" ) + auto_mail_send = fields.Boolean(string="Automatically Send Mail", + config_parameter="meeting_summarizer.auto_mail_send") + select_user = fields.Selection( + selection=[ + ('host', 'Host'), + ('all_attendees', 'All Attendees'), + ], string="Select Recipients", config_parameter="meeting_summarizer.select_user") diff --git a/meeting_summarizer/models/send_mail_transcription.py b/meeting_summarizer/models/send_mail_transcription.py new file mode 100644 index 000000000..3a85ef5f2 --- /dev/null +++ b/meeting_summarizer/models/send_mail_transcription.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU AFFERO +# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. +# +# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE +# (AGPL v3) along with this program. +# If not, see . +# +############################################################################# +""" +This module defines a transient model for sending transcription files via email in Odoo. + +The `SendMailTranscription` model facilitates the creation of a wizard where users can: +- Specify the recipient's email address. +- Enter the subject and body of the email. +- Choose whether to attach a transcription file. + +This module also includes the necessary logic for sending the email, potentially +using predefined email templates. +""" +from odoo import fields, models + + +class SendMailTranscription(models.TransientModel): + """Created a new transient model for send mail""" + _name = 'send.mail.transcription' + _description = "Display send mail transcription wizard details" + + partner_ids = fields.Many2many('res.partner', string='Recipients', + help="Select multiple recipients") + subject = fields.Char(string='Subject', required=True, + help="Subject of the email") + email_body = fields.Html(required=True, String="Content", + help="Body of the email") + transcription_attachment_ids = fields.Many2many( + 'ir.attachment','transcription_attachment_rel', string='Transcription', readonly=True, + help="Transcription attachment ") + summary_attachment_ids = fields.Many2many( + 'ir.attachment','summary_attachment_rel', string='Summary', readonly=True, + help="Summary attachment ") + + def action_send_transcription(self): + """Button action to send mail to the corresponding users that select in recipient mail""" + transcription_id = self.id + email_list = [','.join(self.partner_ids.mapped( + 'email'))] # Collect emails from all partners + from_mail = self.env.user.email + mail_template = (self.env.ref( + 'meeting_summarizer.email_template_transcription')) + attachment_ids = [] + + for attachment in self.transcription_attachment_ids: + attachment_ids.append( + (4, attachment.id)) + + # Loop through summary attachments and add them + for attachment in self.summary_attachment_ids: + attachment_ids.append( + (4, attachment.id)) # (4, attachment_id) adds attachment + + email_values = { + 'email_from': from_mail, + 'email_to': ','.join(email_list), + 'subject': self.subject, + 'body_html': self.email_body, + 'attachment_ids': attachment_ids, # Attach the files here + } + # Send the email using the template + mail_template.send_mail(transcription_id, email_values=email_values, + force_send=True) diff --git a/meeting_summarizer/security/ir.model.access.csv b/meeting_summarizer/security/ir.model.access.csv new file mode 100644 index 000000000..a4e47d261 --- /dev/null +++ b/meeting_summarizer/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_send_mail_transcription_all,access_send.mail.transcription,model_send_mail_transcription,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/meeting_summarizer/static/description/assets/cybro-icon.png b/meeting_summarizer/static/description/assets/cybro-icon.png new file mode 100644 index 000000000..06e73e11d Binary files /dev/null and b/meeting_summarizer/static/description/assets/cybro-icon.png differ diff --git a/meeting_summarizer/static/description/assets/cybro-odoo.png b/meeting_summarizer/static/description/assets/cybro-odoo.png new file mode 100644 index 000000000..ed02e07a4 Binary files /dev/null and b/meeting_summarizer/static/description/assets/cybro-odoo.png differ diff --git a/meeting_summarizer/static/description/assets/icons/arrows-repeat.svg b/meeting_summarizer/static/description/assets/icons/arrows-repeat.svg new file mode 100644 index 000000000..1d7efabc5 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/arrows-repeat.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/banner-1.png b/meeting_summarizer/static/description/assets/icons/banner-1.png new file mode 100644 index 000000000..c180db172 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/banner-1.png differ diff --git a/meeting_summarizer/static/description/assets/icons/banner-2.svg b/meeting_summarizer/static/description/assets/icons/banner-2.svg new file mode 100644 index 000000000..e606d97d9 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/banner-2.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/banner-bg.png b/meeting_summarizer/static/description/assets/icons/banner-bg.png new file mode 100644 index 000000000..a8238d3c0 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/banner-bg.png differ diff --git a/meeting_summarizer/static/description/assets/icons/banner-bg.svg b/meeting_summarizer/static/description/assets/icons/banner-bg.svg new file mode 100644 index 000000000..b1378103e --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/banner-bg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/banner-call.svg b/meeting_summarizer/static/description/assets/icons/banner-call.svg new file mode 100644 index 000000000..96c687e81 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/banner-call.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/banner-mail.svg b/meeting_summarizer/static/description/assets/icons/banner-mail.svg new file mode 100644 index 000000000..cbf0d158d --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/banner-mail.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/banner-pattern.svg b/meeting_summarizer/static/description/assets/icons/banner-pattern.svg new file mode 100644 index 000000000..9c1c7e101 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/banner-pattern.svg @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/banner-promo.svg b/meeting_summarizer/static/description/assets/icons/banner-promo.svg new file mode 100644 index 000000000..d52791b11 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/banner-promo.svg @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/brand-pair.svg b/meeting_summarizer/static/description/assets/icons/brand-pair.svg new file mode 100644 index 000000000..d8db7fc1e --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/brand-pair.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/check.png b/meeting_summarizer/static/description/assets/icons/check.png new file mode 100644 index 000000000..c8e85f51d Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/check.png differ diff --git a/meeting_summarizer/static/description/assets/icons/chevron.png b/meeting_summarizer/static/description/assets/icons/chevron.png new file mode 100644 index 000000000..2089293d6 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/chevron.png differ diff --git a/meeting_summarizer/static/description/assets/icons/close-icon.svg b/meeting_summarizer/static/description/assets/icons/close-icon.svg new file mode 100644 index 000000000..df8cce37a --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/close-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/cogs.png b/meeting_summarizer/static/description/assets/icons/cogs.png new file mode 100644 index 000000000..95d0bad62 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/cogs.png differ diff --git a/meeting_summarizer/static/description/assets/icons/collabarate-icon.svg b/meeting_summarizer/static/description/assets/icons/collabarate-icon.svg new file mode 100644 index 000000000..dd4e10518 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/collabarate-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/meeting_summarizer/static/description/assets/icons/consultation.png b/meeting_summarizer/static/description/assets/icons/consultation.png new file mode 100644 index 000000000..8319d4baa Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/consultation.png differ diff --git a/meeting_summarizer/static/description/assets/icons/cybro-logo.png b/meeting_summarizer/static/description/assets/icons/cybro-logo.png new file mode 100644 index 000000000..ff4b78220 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/cybro-logo.png differ diff --git a/meeting_summarizer/static/description/assets/icons/down.svg b/meeting_summarizer/static/description/assets/icons/down.svg new file mode 100644 index 000000000..f21c36271 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/meeting_summarizer/static/description/assets/icons/ecom-black.png b/meeting_summarizer/static/description/assets/icons/ecom-black.png new file mode 100644 index 000000000..a9385ff13 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/ecom-black.png differ diff --git a/meeting_summarizer/static/description/assets/icons/education-black.png b/meeting_summarizer/static/description/assets/icons/education-black.png new file mode 100644 index 000000000..3eb09b27b Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/education-black.png differ diff --git a/meeting_summarizer/static/description/assets/icons/faq.png b/meeting_summarizer/static/description/assets/icons/faq.png new file mode 100644 index 000000000..4250b5b81 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/faq.png differ diff --git a/meeting_summarizer/static/description/assets/icons/feature-icon.svg b/meeting_summarizer/static/description/assets/icons/feature-icon.svg new file mode 100644 index 000000000..fa0ea6850 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/feature-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/feature.png b/meeting_summarizer/static/description/assets/icons/feature.png new file mode 100644 index 000000000..ac7a785c0 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/feature.png differ diff --git a/meeting_summarizer/static/description/assets/icons/gear.svg b/meeting_summarizer/static/description/assets/icons/gear.svg new file mode 100644 index 000000000..0cc66b6ea --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/gear.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/hire-odoo.svg b/meeting_summarizer/static/description/assets/icons/hire-odoo.svg new file mode 100644 index 000000000..e1ac089b0 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/hire-odoo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/hotel-black.png b/meeting_summarizer/static/description/assets/icons/hotel-black.png new file mode 100644 index 000000000..130f613be Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/hotel-black.png differ diff --git a/meeting_summarizer/static/description/assets/icons/license.png b/meeting_summarizer/static/description/assets/icons/license.png new file mode 100644 index 000000000..a5869797e Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/license.png differ diff --git a/meeting_summarizer/static/description/assets/icons/life-ring-icon.svg b/meeting_summarizer/static/description/assets/icons/life-ring-icon.svg new file mode 100644 index 000000000..3ae6e1d89 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/life-ring-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/lifebuoy.png b/meeting_summarizer/static/description/assets/icons/lifebuoy.png new file mode 100644 index 000000000..658d56ccc Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/lifebuoy.png differ diff --git a/meeting_summarizer/static/description/assets/icons/mail.svg b/meeting_summarizer/static/description/assets/icons/mail.svg new file mode 100644 index 000000000..1eedde695 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/mail.svg @@ -0,0 +1,3 @@ + + + diff --git a/meeting_summarizer/static/description/assets/icons/manufacturing-black.png b/meeting_summarizer/static/description/assets/icons/manufacturing-black.png new file mode 100644 index 000000000..697eb0e9f Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/manufacturing-black.png differ diff --git a/meeting_summarizer/static/description/assets/icons/notes.png b/meeting_summarizer/static/description/assets/icons/notes.png new file mode 100644 index 000000000..ee5e95404 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/notes.png differ diff --git a/meeting_summarizer/static/description/assets/icons/notification icon.svg b/meeting_summarizer/static/description/assets/icons/notification icon.svg new file mode 100644 index 000000000..053189973 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/notification icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/odoo-consultancy.svg b/meeting_summarizer/static/description/assets/icons/odoo-consultancy.svg new file mode 100644 index 000000000..e05f65bde --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/odoo-consultancy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/meeting_summarizer/static/description/assets/icons/odoo-licencing.svg b/meeting_summarizer/static/description/assets/icons/odoo-licencing.svg new file mode 100644 index 000000000..2606c88b0 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/odoo-licencing.svg @@ -0,0 +1,3 @@ + + + diff --git a/meeting_summarizer/static/description/assets/icons/odoo-logo.png b/meeting_summarizer/static/description/assets/icons/odoo-logo.png new file mode 100644 index 000000000..0e4d0eb5a Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/odoo-logo.png differ diff --git a/meeting_summarizer/static/description/assets/icons/patter.svg b/meeting_summarizer/static/description/assets/icons/patter.svg new file mode 100644 index 000000000..25c9c0a8f --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/patter.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/pattern1.png b/meeting_summarizer/static/description/assets/icons/pattern1.png new file mode 100644 index 000000000..09ab0fb2d Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/pattern1.png differ diff --git a/meeting_summarizer/static/description/assets/icons/pos-black.png b/meeting_summarizer/static/description/assets/icons/pos-black.png new file mode 100644 index 000000000..97c0f90c1 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/pos-black.png differ diff --git a/meeting_summarizer/static/description/assets/icons/puzzle-piece-icon.svg b/meeting_summarizer/static/description/assets/icons/puzzle-piece-icon.svg new file mode 100644 index 000000000..3e9ad9373 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/puzzle-piece-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/puzzle.png b/meeting_summarizer/static/description/assets/icons/puzzle.png new file mode 100644 index 000000000..65cf854e7 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/puzzle.png differ diff --git a/meeting_summarizer/static/description/assets/icons/replace-icon.svg b/meeting_summarizer/static/description/assets/icons/replace-icon.svg new file mode 100644 index 000000000..d0e3a7af1 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/replace-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/restaurant-black.png b/meeting_summarizer/static/description/assets/icons/restaurant-black.png new file mode 100644 index 000000000..4a35eb939 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/restaurant-black.png differ diff --git a/meeting_summarizer/static/description/assets/icons/screenshot-main.png b/meeting_summarizer/static/description/assets/icons/screenshot-main.png new file mode 100644 index 000000000..575f8e676 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/screenshot-main.png differ diff --git a/meeting_summarizer/static/description/assets/icons/screenshot.png b/meeting_summarizer/static/description/assets/icons/screenshot.png new file mode 100644 index 000000000..cef272529 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/screenshot.png differ diff --git a/meeting_summarizer/static/description/assets/icons/service-black.png b/meeting_summarizer/static/description/assets/icons/service-black.png new file mode 100644 index 000000000..301ab51cb Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/service-black.png differ diff --git a/meeting_summarizer/static/description/assets/icons/skype-fill.svg b/meeting_summarizer/static/description/assets/icons/skype-fill.svg new file mode 100644 index 000000000..c17423639 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/skype-fill.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/skype.png b/meeting_summarizer/static/description/assets/icons/skype.png new file mode 100644 index 000000000..51b409fb3 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/skype.png differ diff --git a/meeting_summarizer/static/description/assets/icons/skype.svg b/meeting_summarizer/static/description/assets/icons/skype.svg new file mode 100644 index 000000000..df3dad39b --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/skype.svg @@ -0,0 +1,3 @@ + + + diff --git a/meeting_summarizer/static/description/assets/icons/star-1.svg b/meeting_summarizer/static/description/assets/icons/star-1.svg new file mode 100644 index 000000000..7e55ab162 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/star-1.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/star-2.svg b/meeting_summarizer/static/description/assets/icons/star-2.svg new file mode 100644 index 000000000..5ae9f507a --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/star-2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/support.png b/meeting_summarizer/static/description/assets/icons/support.png new file mode 100644 index 000000000..4f18b8b82 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/support.png differ diff --git a/meeting_summarizer/static/description/assets/icons/test-1 - Copy.png b/meeting_summarizer/static/description/assets/icons/test-1 - Copy.png new file mode 100644 index 000000000..f6a902663 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/test-1 - Copy.png differ diff --git a/meeting_summarizer/static/description/assets/icons/test-1.png b/meeting_summarizer/static/description/assets/icons/test-1.png new file mode 100644 index 000000000..0908add2b Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/test-1.png differ diff --git a/meeting_summarizer/static/description/assets/icons/test-2.png b/meeting_summarizer/static/description/assets/icons/test-2.png new file mode 100644 index 000000000..4671fe91e Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/test-2.png differ diff --git a/meeting_summarizer/static/description/assets/icons/trading-black.png b/meeting_summarizer/static/description/assets/icons/trading-black.png new file mode 100644 index 000000000..9398ba2f1 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/trading-black.png differ diff --git a/meeting_summarizer/static/description/assets/icons/training.png b/meeting_summarizer/static/description/assets/icons/training.png new file mode 100644 index 000000000..884ca024d Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/training.png differ diff --git a/meeting_summarizer/static/description/assets/icons/translate.svg b/meeting_summarizer/static/description/assets/icons/translate.svg new file mode 100644 index 000000000..af9c8a1aa --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/translate.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/update.png b/meeting_summarizer/static/description/assets/icons/update.png new file mode 100644 index 000000000..ecbc5a01a Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/update.png differ diff --git a/meeting_summarizer/static/description/assets/icons/user.png b/meeting_summarizer/static/description/assets/icons/user.png new file mode 100644 index 000000000..6ffb23d9f Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/user.png differ diff --git a/meeting_summarizer/static/description/assets/icons/video.png b/meeting_summarizer/static/description/assets/icons/video.png new file mode 100644 index 000000000..576705b17 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/video.png differ diff --git a/meeting_summarizer/static/description/assets/icons/whatsapp.png b/meeting_summarizer/static/description/assets/icons/whatsapp.png new file mode 100644 index 000000000..d513a5356 Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/whatsapp.png differ diff --git a/meeting_summarizer/static/description/assets/icons/wrench-icon.svg b/meeting_summarizer/static/description/assets/icons/wrench-icon.svg new file mode 100644 index 000000000..174b5a465 --- /dev/null +++ b/meeting_summarizer/static/description/assets/icons/wrench-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/meeting_summarizer/static/description/assets/icons/wrench.png b/meeting_summarizer/static/description/assets/icons/wrench.png new file mode 100644 index 000000000..6c04dea0f Binary files /dev/null and b/meeting_summarizer/static/description/assets/icons/wrench.png differ diff --git a/meeting_summarizer/static/description/assets/modules/b1.png b/meeting_summarizer/static/description/assets/modules/b1.png new file mode 100644 index 000000000..a142dca98 Binary files /dev/null and b/meeting_summarizer/static/description/assets/modules/b1.png differ diff --git a/meeting_summarizer/static/description/assets/modules/b2.png b/meeting_summarizer/static/description/assets/modules/b2.png new file mode 100644 index 000000000..dc95f5e69 Binary files /dev/null and b/meeting_summarizer/static/description/assets/modules/b2.png differ diff --git a/meeting_summarizer/static/description/assets/modules/b3.png b/meeting_summarizer/static/description/assets/modules/b3.png new file mode 100644 index 000000000..6b2880bbf Binary files /dev/null and b/meeting_summarizer/static/description/assets/modules/b3.png differ diff --git a/meeting_summarizer/static/description/assets/modules/b4.png b/meeting_summarizer/static/description/assets/modules/b4.png new file mode 100644 index 000000000..a9744b1bd Binary files /dev/null and b/meeting_summarizer/static/description/assets/modules/b4.png differ diff --git a/meeting_summarizer/static/description/assets/modules/b5.png b/meeting_summarizer/static/description/assets/modules/b5.png new file mode 100644 index 000000000..31eee94ee Binary files /dev/null and b/meeting_summarizer/static/description/assets/modules/b5.png differ diff --git a/meeting_summarizer/static/description/assets/modules/b6.png b/meeting_summarizer/static/description/assets/modules/b6.png new file mode 100644 index 000000000..2e497233e Binary files /dev/null and b/meeting_summarizer/static/description/assets/modules/b6.png differ diff --git a/meeting_summarizer/static/description/assets/screenshots/1.png b/meeting_summarizer/static/description/assets/screenshots/1.png new file mode 100644 index 000000000..29adebc4f Binary files /dev/null and b/meeting_summarizer/static/description/assets/screenshots/1.png differ diff --git a/meeting_summarizer/static/description/assets/screenshots/2.png b/meeting_summarizer/static/description/assets/screenshots/2.png new file mode 100644 index 000000000..5f28da01b Binary files /dev/null and b/meeting_summarizer/static/description/assets/screenshots/2.png differ diff --git a/meeting_summarizer/static/description/assets/screenshots/3.png b/meeting_summarizer/static/description/assets/screenshots/3.png new file mode 100644 index 000000000..8b39e538d Binary files /dev/null and b/meeting_summarizer/static/description/assets/screenshots/3.png differ diff --git a/meeting_summarizer/static/description/assets/screenshots/4.png b/meeting_summarizer/static/description/assets/screenshots/4.png new file mode 100644 index 000000000..b200c2b68 Binary files /dev/null and b/meeting_summarizer/static/description/assets/screenshots/4.png differ diff --git a/meeting_summarizer/static/description/assets/screenshots/hero.gif b/meeting_summarizer/static/description/assets/screenshots/hero.gif new file mode 100644 index 000000000..49931d86a Binary files /dev/null and b/meeting_summarizer/static/description/assets/screenshots/hero.gif differ diff --git a/meeting_summarizer/static/description/banner.jpg b/meeting_summarizer/static/description/banner.jpg new file mode 100644 index 000000000..dd5316f30 Binary files /dev/null and b/meeting_summarizer/static/description/banner.jpg differ diff --git a/meeting_summarizer/static/description/icon.png b/meeting_summarizer/static/description/icon.png new file mode 100644 index 000000000..576bf0370 Binary files /dev/null and b/meeting_summarizer/static/description/icon.png differ diff --git a/meeting_summarizer/static/description/index.html b/meeting_summarizer/static/description/index.html new file mode 100644 index 000000000..24aa6553b --- /dev/null +++ b/meeting_summarizer/static/description/index.html @@ -0,0 +1,890 @@ + + + + + + Meeting Summarizer + + + + + + + + + + +
+
+ + + +
+
+ Community +
+
+ Enterprise +
+
+
+ +
+
+
+
+

+ This module helps to generate summaries of meetings by transcribing and processing conversations in real time.

+

Meeting Summarizer +

+
+
+ +
+ +
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+

Key + Highlights

+
+
+
+
+ +
+
+ Retrieve and download the transcription data file. +
+

+

+
+
+
+
+
+ +
+
+ Get and download the transcription summary file. +
+

+

+
+
+
+
+
+ +
+
+ Send the transcription and summary files automatically to the selected users. +
+

+

+
+
+
+
+
+ +
+
+ Send the transcription and summary files manually to the selected users as well. +
+

+

+
+
+
+
+ +
+
+
+ Meeting Summarizer +

+ Are you ready to make your business more + organized? +
Improve now! +

+ +
+
+ +
+
+
+ + + +
+
+ +
+
+
+
+ acc_bg +
+ +
+
+
+
+

+ Add Open API Key + +

+
+
+

+ First, set the OpenAPI key to enable transcription summary creation.

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Manually send the transcription and summary files. + +

+
+
+

+ When a new meeting starts with participants and the call end button is clicked, a wizard will automatically open with the transcription and summary files attached. The meeting description will be set as the subject. + These files can be sent to multiple participants by clicking the Send Mail button. +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ + Automatically send the transcription and summary files. + +

+
+
+

When the 'Automatically Send Mail' feature is enabled in General Settings, + the system can automatically send the files based on the selected option. If 'Host' is selected, the files will be sent to the person who created the meeting. If 'All Attendees' is selected, + the files will be sent to all participants who attended the meeting, excluding public users.

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ + +

+
+
+

The meeting contents are emailed in the following format.

+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+

+ Send transcription and summary files manually or automatically.

+
+ +
+
+
+
+
+
+
+
+
+ +
+ +
+ +
+

+ This feature allows you + to add custom questions + related to the product during + the ordering process in a POS session. +

+
+
+ +
+ +
+
+
+
+
+
+ +
+
+

+ Latest Release 18.0.1.0.0 +

+ + 14th May, 2025 + +
+
+
+
+
+ Add +
+
+
+
    +
  • + Initial Commit +
  • +
+
+
+
+
+
+
+
+
+
+ + + +
+

+ Related Products +

+ +
+ +
+

+ Our Services

+ +
+ +
+
+ .... +
+
+ +
+ + +
+
+ + + + + + diff --git a/meeting_summarizer/static/src/js/attachment_list.js b/meeting_summarizer/static/src/js/attachment_list.js new file mode 100644 index 000000000..1d6b897dc --- /dev/null +++ b/meeting_summarizer/static/src/js/attachment_list.js @@ -0,0 +1,17 @@ +/* @odoo-module */ +import { AttachmentList } from "@mail/core/common/attachment_list"; +import { registry } from "@web/core/registry"; +import { patch } from "@web/core/utils/patch"; +import { user } from "@web/core/user"; +import { useState } from "@odoo/owl"; +const ImageActions = AttachmentList.components.ImageActions; + +const customImage = { + setup(){ + super.setup(); + this.state = useState({ + isPublicUser : user.userId === null + }); + } +}; +patch(AttachmentList.prototype, customImage); \ No newline at end of file diff --git a/meeting_summarizer/static/src/js/call_action_list.js b/meeting_summarizer/static/src/js/call_action_list.js new file mode 100644 index 000000000..5d280402b --- /dev/null +++ b/meeting_summarizer/static/src/js/call_action_list.js @@ -0,0 +1,580 @@ +/* @odoo-module */ +import { patch } from "@web/core/utils/patch"; +import { CallActionList } from "@mail/discuss/call/common/call_action_list"; +import { useService } from "@web/core/utils/hooks"; +import { useState, useEffect } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; +import { user } from "@web/core/user"; + +class SpeechRecognitionQueue { + constructor(transcriptionHandler, user) { + this.transcriptionHandler = transcriptionHandler; + this.shouldRestart = true; + this.user = user; + this.isInitialized = false; + this.active = false; + this.silenceTimer = null; + this.bufferText = ""; + this.isFinal = false; + this.stopCall = false; + this.recognitionActive = false; + } + + async initSpeechRecognition() { + if (this.isInitialized) return; + + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + if (!SpeechRecognition) { + console.warn("Speech Recognition not supported in this browser"); + return; + } + + try { + // Request audio permissions explicitly + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + // Keep track of the stream to ensure it stays active + this.audioStream = stream; + this.recognition = new SpeechRecognition(); + this.recognition.continuous = true; + this.recognition.interimResults = true; + this.recognition.lang = 'en-US'; + this.setupEventListeners(); + this.isInitialized = true; + } catch (error) { + console.error("Error initializing speech recognition:", error); + this.shouldRestart = false; + // Provide more specific error feedback + if (error.name === 'NotAllowedError') { + console.error("Microphone permission denied. Please enable microphone access."); + } + } + } + + setupEventListeners() { + this.recognition.onerror = (event) => { + console.error("Speech Recognition Error:", event.error, event); + if (event.error === 'not-allowed') { + this.shouldRestart = false; + console.error("Microphone permission denied"); + } else if (event.error === 'audio-capture') { + this.shouldRestart = false; + console.error("No microphone detected"); + } else if (this.shouldRestart && this.active) { + // Use exponential backoff for retries + const delay = Math.min(2000, 1000 * Math.pow(1.5, this.errorCount || 0)); + this.errorCount = (this.errorCount || 0) + 1; + setTimeout(() => this.restartRecognition(), delay); + } + }; + + this.recognition.onend = () => { + this.recognitionActive = false; + // Process any remaining buffered text + if (this.bufferText.trim()) { + this.transcriptionHandler({ + text: this.bufferText.trim(), + userId: this.user?.id, + timestamp: new Date().toISOString() + }); + this.bufferText = ""; + } + + if (this.shouldRestart && this.active) { + // Add a small delay before restarting + setTimeout(() => this.restartRecognition(), 300); + } + }; + + this.recognition.onstart = () => { + this.recognitionActive = true; + this.errorCount = 0; // Reset error count on successful start + }; + + this.recognition.onresult = (event) => { + let finalTranscript = ""; + let interimTranscript = ""; + let hasFinal = false; + + // Process all results since last event + for (let i = event.resultIndex; i < event.results.length; i++) { + const transcript = event.results[i][0].transcript; + + if (event.results[i].isFinal) { + finalTranscript += transcript; + hasFinal = true; + } else { + interimTranscript += transcript; + } + } + + // Handle final results + if (hasFinal && finalTranscript.trim()) { + this.bufferText += finalTranscript; + + // Send complete utterances + const completeUtterances = this.extractCompleteUtterances(this.bufferText); + if (completeUtterances) { + this.transcriptionHandler({ + text: completeUtterances, + userId: this.user?.id, + timestamp: new Date().toISOString() + }); + + // Remove sent utterances from buffer + this.bufferText = this.bufferText.slice(completeUtterances.length); + } + } + + // Reset silence timer on any speech + if (this.silenceTimer) { + clearTimeout(this.silenceTimer); + } + + // Set silence timer to send remaining buffer after pause + this.silenceTimer = setTimeout(() => { + if (this.bufferText.trim()) { + this.transcriptionHandler({ + text: this.bufferText.trim(), + userId: this.user?.id, + timestamp: new Date().toISOString() + }); + this.bufferText = ""; + } + }, 2000); // 2 second silence sends buffer + }; + } + + // Helper to extract complete utterances (ending with punctuation) + extractCompleteUtterances(text) { + if (!text) return ""; + + // Find the last occurrence of sentence-ending punctuation + const match = text.match(/[.!?]\s*(?=[A-Z]|$)/); + if (!match) return ""; + + const endPos = match.index + 1; + return text.substring(0, endPos + 1).trim(); + } + + async restartRecognition() { + if (!this.active || !this.shouldRestart) return; + try { + // Only stop if actually running + if (this.recognitionActive) { + try { + await this.recognition.stop(); + // Wait for onend event to complete + await new Promise(resolve => { + const checkActive = () => { + if (!this.recognitionActive) { + resolve(); + } else { + setTimeout(checkActive, 100); + } + }; + checkActive(); + }); + } catch (error) { + console.log("Error stopping recognition:", error); + } + } + + // Small delay to ensure complete shutdown + await new Promise(resolve => setTimeout(resolve, 500)); + + // Only try to start if we're still supposed to be active + if (this.active && this.shouldRestart) { + await this.recognition.start(); + } + } catch (error) { + console.error("Error in restartRecognition:", error); + + // Try again after a longer delay if we're still supposed to be active + if (this.active && this.shouldRestart) { + setTimeout(() => this.restartRecognition(), 2000); + } + } + } + + async start() { + if (!this.isInitialized) { + await this.initSpeechRecognition(); + } + + if (this.recognition) { + this.shouldRestart = true; // Reset the flag when starting + this.active = true; + this.bufferText = ""; // Clear any previous buffer + this.errorCount = 0; + + try { + // Only start if not already running + if (!this.recognitionActive) { + await this.recognition.start(); + } else { + console.log("Recognition already active, not starting again"); + } + } catch (error) { + if (error.name === 'NotAllowedError') { + this.shouldRestart = false; + // Request permissions again + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + this.audioStream = stream; + // Try starting again after permission granted + setTimeout(() => this.start(), 1000); + } catch (permError) { + console.error("Permission request failed:", permError); + } + } else if (this.active) { + // Try again after a delay + setTimeout(() => this.start(), 1000); + } + } + } else { + console.error("Recognition not initialized properly"); + } + } + + stop() { + this.active = false; + this.stopCall = true; + this.shouldRestart = false; // This prevents restarting + + if (this.silenceTimer) { + clearTimeout(this.silenceTimer); + this.silenceTimer = null; + } + + // Process any remaining buffer + if (this.bufferText.trim()) { + this.transcriptionHandler({ + text: this.bufferText.trim(), + userId: this.user?.id, + timestamp: new Date().toISOString() + }); + this.bufferText = ""; + } + + if (this.recognition && this.recognitionActive) { + try { + this.recognition.stop(); + } catch (error) { + console.error("Error stopping recognition:", error); + } + } + + // Release audio stream if it exists + if (this.audioStream) { + this.audioStream.getTracks().forEach(track => track.stop()); + this.audioStream = null; + } + } + + reset() { + this.shouldRestart = true; + this.active = false; + this.bufferText = ""; + this.errorCount = 0; + + if (this.silenceTimer) { + clearTimeout(this.silenceTimer); + this.silenceTimer = null; + } + + // Stop any active recognition + if (this.recognition && this.recognitionActive) { + try { + this.recognition.stop(); + } catch (error) { + console.error("Error stopping recognition during reset:", error); + } + } + } +} + +patch(CallActionList.prototype, { + async setup() { + super.setup(...arguments); + try { + this.action = useService("action"); + this.user = user; + } catch (error) { + console.warn("Service 'action' is not available for guest users."); + this.action = null; // Fallback for guests + this.user = null; // Fallback for guests + } + + this.state = useState({ + isRecording: false, + transcriptions: {}, + currentSpeaker: null, + userName: null, + }); + + useEffect(() => { + this.speechQueue = new SpeechRecognitionQueue( + this.insertTranscription.bind(this), + this.user + ); + this.initializeUserInfo(); + + // Cleanup function for when component is destroyed + return () => { + if (this.speechQueue) { + this.speechQueue.stop(); + } + }; + }, () => []); + + useEffect(() => { + try { + const hasMultipleMembers = this.props.thread && + this.props.thread.channelMembers && + this.props.thread.channelMembers.length > 0; + + + if (hasMultipleMembers && !this.state.isRecording) { + this.startRecording(); + } else if (!hasMultipleMembers && this.state.isRecording) { + this.stopRecording(); + } + } catch (error) { + console.error("Error in channel members effect:", error); + } + }, () => [this.props.thread?.channelMembers?.length]); + }, + + async onClickMicrophone(ev) { + await super.onClickMicrophone(ev); + + try { + if (this.rtc?.state?.selfSession?.isMute) { + this.stopRecording(); + } else { + this.startRecording(); + } + } catch (error) { + console.error("Error handling microphone click:", error); + } + }, + + async initializeUserInfo() { + try { + let userName = "Unknown User"; // Default fallback + + // Try multiple methods to get user name + if (this?.user?.name) { + userName = this.user.name; + } else if (this?.__owl__?.parent?.parent?.children?.__11?.component?.state?.value) { + userName = this.__owl__.parent.parent.children.__11.component.state.value; + } + + this.state.userName = userName; + } catch (error) { + console.error("Error initializing user info:", error); + this.state.userName = "Unknown User"; // Fallback + } + }, + + async CreateTranscriptionFile() { + try { + if (!this.props.thread || !this.props.thread.id) { + console.error("Cannot create transcription file: thread or thread ID is missing"); + return; + } + + const fileName = `transcription_${this.props.thread.id}.txt`; + const Id = this.props.thread.id; + + const response = await rpc('/create/transcription_file_summary', { + kwargs: { id: Id } + }); + } catch (error) { + console.error("Error creating transcription file:", error); + } + }, + + async onClickToggleAudioCall(ev) { + try { + const buttonDis = document.querySelector('[aria-label="Disconnect"]'); + const res = await super.onClickToggleAudioCall(ev); + if (!this.props.thread) { + console.error("Thread is undefined in onClickToggleAudioCall"); + return res; + } + + const subject = this.props.thread.description || " "; + await this.CreateTranscriptionFile(); + + setTimeout(async () => { + try { + const result = await rpc('/get/transcription_data/summary', { + kwargs: { + channelId: this.props.thread.id, + }, + }); + + const MeetingAdmin = await rpc('/get/Meeting/creator', { + kwargs: { + channelId: this.props.thread.id, + }, + }); + if ((this.user?.userId == MeetingAdmin) && buttonDis && result?.transcriptionId && result?.summaryId) { + try { + this.stopRecording(); + + const partner_details = await rpc('/check/auto_mail_send', { + kwargs: { + channelId: this.props.thread.id, + }, + }); + + const partnerIds = partner_details.map(p => p.partner_id); + + const transcriptionId = await rpc('/create/send_transcription/record', { + kwargs: { + partnerIds: partnerIds, + subject: subject, + email_body: "

Meeting content here...

", + transcriptionId: result.transcriptionId, + summaryId: result.summaryId, + }, + }); + + if (partner_details.length != 0) { + const partners_email = partner_details.map(p => p.email); + + const emailResponse = await rpc('/send/auto_email', { + kwargs: { + partners_email: partners_email, + subject: subject, + email_body: "

Meeting content here...

", + transcriptionId: result.transcriptionId, + summaryId: result.summaryId, + }, + }); + + } else { + await this.tryOpeningTranscription(transcriptionId); + } + } catch (error) { + console.error("Error in admin meeting end processing:", error); + } + } + } catch (error) { + console.error("Error in transcription processing timeout:", error); + } + }, 500); + + return res; + } catch (error) { + console.error("Error in onClickToggleAudioCall:", error); + return false; + } + }, + + async tryOpeningTranscription(transcriptionId) { + if (!this.action) { + console.error("Error: this.action is undefined."); + return; + } + + try { + const actionData = { + name: "Send mail Transcription", + type: "ir.actions.act_window", + res_model: "send.mail.transcription", + res_id: transcriptionId, + view_mode: "form", + views: [[false, "form"]], // Ensures proper view format + target: "new", + }; + + await this.action.doAction(actionData); + } catch (error) { + console.error("Error in doAction:", error); + } + }, + + async startRecording() { + try { + if (!this.speechQueue) { + this.speechQueue = new SpeechRecognitionQueue( + this.insertTranscription.bind(this), + this.user + ); + } + + await this.speechQueue.start(); + this.state.isRecording = true; + this.state.currentSpeaker = this.state.userName || (this.user?.name || "Unknown User"); + + if (!this.state.transcriptions[this.state.currentSpeaker]) { + this.state.transcriptions[this.state.currentSpeaker] = []; + } + + } catch (error) { + console.error("Error starting recording:", error); + this.state.isRecording = false; + } + }, + + async insertTranscription(transcriptionData) { + const { text, userId, timestamp } = transcriptionData; + + let speakerName = "Unknown User"; + + // Try multiple methods to get speaker name + if (this.state.userName) { + speakerName = this.state.userName; + } else if (this.user && this.user.name) { + speakerName = this.user.name; + } + + const formattedText = `(${new Date(timestamp).toLocaleString()})\n\t${speakerName} : ${text}\n`; + + try { + if (!this.props.thread || !this.props.thread.id) { + console.error("Cannot send transcription: thread or thread ID is missing"); + return; + } + + const response = await rpc('/get/transcription_data', { + data: formattedText, + id: this.props.thread.id, + userId: userId, + timestamp: timestamp + }); + + } catch (error) { + console.error("Error sending transcription:", error); + } + }, + + async stopRecording() { + + if (this.state.isRecording) { + if (this.speechQueue) { + this.speechQueue.stop(); + } + this.state.isRecording = false; + } + + try { + if (!this.props.thread || !this.props.thread.id) { + console.error("Cannot attach transcription: thread or thread ID is missing"); + return; + } + + await rpc('/attach/transcription_data/summary', { + kwargs: { + channelId: this.props.thread.id, + }, + }); + + } catch (error) { + console.error("Error attaching transcription data:", error); + } + }, +}); \ No newline at end of file diff --git a/meeting_summarizer/static/src/xml/attachment_list.xml b/meeting_summarizer/static/src/xml/attachment_list.xml new file mode 100644 index 000000000..30212b9b2 --- /dev/null +++ b/meeting_summarizer/static/src/xml/attachment_list.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/meeting_summarizer/views/res_config_settings.xml b/meeting_summarizer/views/res_config_settings.xml new file mode 100644 index 000000000..54210f6c7 --- /dev/null +++ b/meeting_summarizer/views/res_config_settings.xml @@ -0,0 +1,29 @@ + + + + res.config.settings.inherit.integrations + res.config.settings + + + + + + +
+ +
+
+ + +
+ +
+
+
+
+
+
+
\ No newline at end of file diff --git a/meeting_summarizer/views/send_mail_transcription.xml b/meeting_summarizer/views/send_mail_transcription.xml new file mode 100644 index 000000000..d774afc89 --- /dev/null +++ b/meeting_summarizer/views/send_mail_transcription.xml @@ -0,0 +1,25 @@ + + + + + send.mail.transcription.view.form + send.mail.transcription + +
+ + + + + + + + + + +
+
+
+
\ No newline at end of file